{"record":{"id":"8f9ff4682c7a9b27","repo":"louis-e/arnis","slug":"land-cover-grid-mismatch","errorCode":null,"errorMessage":"land cover grid mismatch","messagePattern":"land cover grid mismatch","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/gui/js/preview3d.js","lineNumber":414,"sourceCode":"    if (!modalView || !modalView.map) return;\n    const map = modalView.map;\n    try {\n      if (map.getLayer(\"landcover\")) {\n        map.setLayoutProperty(\"landcover\", \"visibility\", enabled ? \"visible\" : \"none\");\n        return;\n      }\n      if (!enabled) return;\n      const d = datasets.get(modalView.gen);\n      if (!d) return;\n\n      toggle.disabled = true;\n      let url = landCoverCache.key === d.bboxText ? landCoverCache.url : null;\n      if (!url) {\n        const raw = await window.__TAURI__.core.invoke(\"gui_get_preview_landcover\", {\n          bboxText: d.bboxText,\n        });\n        const { buffer, gw, gh } = readPayloadHeader(raw, \"APL1\");\n        if (gw !== d.gw || gh !== d.gh) throw new Error(\"land cover grid mismatch\");\n        url = landCoverUrl(new Uint8Array(buffer, 12, gw * gh), gw, gh);\n        landCoverCache = { key: d.bboxText, url: url };\n      }\n      if (!modalView || modalView.map !== map) return;\n      map.addSource(\"landcover\", {\n        type: \"image\",\n        url: url,\n        coordinates: [\n          [d.minLng, d.maxLat],\n          [d.maxLng, d.maxLat],\n          [d.maxLng, d.minLat],\n          [d.minLng, d.minLat],\n        ],\n      });\n      map.addLayer(\n        {\n          id: \"landcover\",\n          type: \"raster\",","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/louis-e/arnis/blob/34048924d9365795fb0d832e76140a3fbdc413d9/src/gui/js/preview3d.js#L396-L432","documentation":"The land cover payload ('APL1') carries its own grid dimensions (gw, gh) in its header; this check asserts they match the DEM dataset's grid (d.gw, d.gh) because the land cover raster is overlaid pixel-for-pixel on the DEM grid. A mismatch means the two rasters cannot be aligned, so the overlay is aborted with this Error.","triggerScenarios":"Calling gui_get_preview_landcover with the dataset's bboxText and receiving a grid whose width/height differ from d.gw/d.gh — e.g. the Rust backend computes land cover at a different resolution, the bbox text maps to a different cached grid, or the DEM dataset was regenerated at a new resolution while landCoverCache still holds a result keyed to the old bboxText but the newly fetched cover uses old dims (or vice versa).","commonSituations":"Backend changed its land cover resolution constant without a matching change to the DEM grid size; user panned/resized the selection so bboxText changed and the DEM was re-fetched at new dims while a stale land cover response arrived for the old dims; concurrent generations interleaving async results (no generation token checked before the dimension comparison).","solutions":["Make the Rust gui_get_preview_landcover command derive its grid dimensions from the same source as the DEM command so gw/gh always match d.gw/d.gh.","Capture the dataset generation/id when starting the land cover fetch and re-validate (or discard) the result if the dataset changed while awaiting.","Invalidate landCoverCache whenever the DEM dataset is regenerated, not only when bboxText changes.","Catch this error in the caller and retry the land cover fetch once after refreshing the dataset dimensions.","Log gw/gh from both payloads on mismatch to quickly identify which side's resolution constant drifted."],"exampleFix":"// before\nconst raw = await window.__TAURI__.core.invoke(\"gui_get_preview_landcover\", { bboxText: d.bboxText });\nconst { buffer, gw, gh } = readPayloadHeader(raw, \"APL1\");\nif (gw !== d.gw || gh !== d.gh) throw new Error(\"land cover grid mismatch\");\n// after\nconst genAtStart = d.gen;\nconst raw = await window.__TAURI__.core.invoke(\"gui_get_preview_landcover\", { bboxText: d.bboxText });\nif (d.gen !== genAtStart) return; // dataset regenerated mid-flight\nconst { buffer, gw, gh } = readPayloadHeader(raw, \"APL1\");\nif (gw !== d.gw || gh !== d.gh) {\n  console.error(\"landcover grid\", gw, gh, \"vs dem\", d.gw, d.gh);\n  landCoverCache = { key: null, url: null };\n  return;\n}","handlingStrategy":"validation","validationCode":"const { buffer, gw, gh } = readPayloadHeader(raw, \"APL1\");\nif (gw !== d.gw || gh !== d.gh) {\n  console.error(\"landcover dims\", { gw, gh, demGw: d.gw, demGh: d.gh });\n  return; // skip overlay instead of throwing\n}","typeGuard":"function dimsMatch(cover, dem) { return cover.gw === dem.gw && cover.gh === dem.gh; }","tryCatchPattern":"try {\n  await loadLandCover(d);\n} catch (e) {\n  if (e.message === \"land cover grid mismatch\") {\n    landCoverCache = { key: null, url: null };\n    return; // render terrain without overlay\n  }\n  throw e;\n}","preventionTips":["Derive land cover and DEM grid dimensions from one shared constant in the Rust backend.","Use a generation token and re-validate the dataset after every await.","Clear landCoverCache whenever the DEM is regenerated, not only on bbox change.","Log both grids' dimensions on mismatch to spot which side drifted."],"tags":["grid-alignment","raster","tauri","race-condition"],"backgroundTag":"grid-dimension-mismatch","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"}