{"record":{"id":"113422b56b5b3d90","repo":"louis-e/arnis","slug":"select-level-for-cell-size-called-with-empty-level","errorCode":null,"errorMessage":"select_level_for_cell_size called with empty levels","messagePattern":"select_level_for_cell_size called with empty levels","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/elevation/providers/fixed_tile.rs","lineNumber":227,"sourceCode":"/// whose native pixels are no more than 1.5× finer than the output\n/// cell. The factor tolerates a modest amount of *downsampling* from\n/// the source (up to 1.5× finer-than-needed) before we give up on it\n/// and step to the next coarser level, which avoids pulling dense\n/// LiDAR tiles we'd immediately average away. Upsampling the other\n/// direction (output finer than source) is unbounded by this rule —\n/// if the user asks for 0.4 m cells on a 1 m source, the condition\n/// `1.0 * 1.5 ≥ 0.4` holds easily and we use the 1 m level with\n/// bilinear fill-in.\n///\n/// `levels` must be ordered finest-to-coarsest. When no level qualifies\n/// the coarsest is returned as a fallback.\npub(super) fn select_level_for_cell_size<R: Resolution + Copy>(\n    levels: &[R],\n    cell_size_m: f64,\n) -> R {\n    if levels.is_empty() {\n        // Caller must configure at least one level; this is a bug.\n        panic!(\"select_level_for_cell_size called with empty levels\");\n    }\n    if !cell_size_m.is_finite() || cell_size_m <= 0.0 {\n        return levels[0];\n    }\n    for &level in levels {\n        if level.meters_per_pixel() * 1.5 >= cell_size_m {\n            return level;\n        }\n    }\n    *levels.last().unwrap()\n}\n\n/// Approximate physical bbox dimensions in meters. Precise enough for\n/// resolution-level selection.\npub(super) fn bbox_dimensions_m(bbox: &LLBBox) -> (f64, f64) {\n    let mid_lat = (bbox.min().lat() + bbox.max().lat()) * 0.5;\n    let mid_lat_cos = mid_lat.to_radians().cos().abs().max(1e-6);\n    let width_deg = bbox.max().lng() - bbox.min().lng();","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/louis-e/arnis/blob/34048924d9365795fb0d832e76140a3fbdc413d9/src/elevation/providers/fixed_tile.rs#L209-L245","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Configure the provider with at least one resolution level (levels ordered finest-to-coarsest) before calling fetch.","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.","If levels come from config deserialization, add a validation step after parsing that rejects empty level lists with a clear user-facing message."],"exampleFix":"// before\nlet provider = FixedTileProvider { levels: vec![] };\nprovider.fetch_fixed_tile_grid(...); // panics\n// after\nlet provider = FixedTileProvider { levels: vec![Level::res1m(), Level::res10m()] };\nassert!(!provider.levels.is_empty(), \"provider requires at least one resolution level\");\nprovider.fetch_fixed_tile_grid(...);","handlingStrategy":"validation","validationCode":"if provider.levels.is_empty() {\n    return Err(\"elevation provider must be configured with at least one resolution level\".into());\n}\nlet grid = provider.fetch_fixed_tile_grid(...)?;","typeGuard":"fn has_levels<R: Resolution>(levels: &[R]) -> bool { !levels.is_empty() }","tryCatchPattern":"// panics are not catchable in normal Rust; validate before calling\nlet result = std::panic::catch_unwind(|| provider.fetch_fixed_tile_grid(...));\nmatch result { Ok(grid) => grid, Err(_) => fallback_grid() }","preventionTips":["Validate provider config (non-empty levels) immediately after construction/deserialization","Keep levels ordered finest-to-coarsest as the API documents","Add a unit test asserting constructor rejects empty level lists"],"tags":["panic","configuration","elevation","rust"],"backgroundTag":"empty-collection-invariant-violation","analyzedSha":"34048924d9365795fb0d832e76140a3fbdc413d9","analyzedAt":"2026-09-03T14:05:17.283Z","contentChangedAt":"2026-09-03T14:05:17.283Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}