Skip to content

ROI and brushing

  1. Press D, or use Draw ROI in the global bar or the palette. The Moon enters drawing mode.
  2. Click to add vertices. Coordinates are geographic lon/lat in degrees on a sphere of radius 1737.4 km (+proj=longlat +R=1737400).
  3. Double-click, or press Enter, to close the ring. Backspace removes the last vertex; Escape cancels the draft.
  4. The ring is sent to Python for evaluation (POST /roi/evaluate). The returned evaluation is the state that the Rows, Cells, Evidence and Figure instruments display.

On a phone or small tablet, tap to add vertices and use the Close ROI, Undo vertex and Cancel drawing buttons under the Moon instead of the keys (see Phones and small tablets).

Constraints enforced by the sidecar:

ConstraintValueFailure
Vertices3 to 512 (closure is implicit)422
Vertextwo finite numbers; latitude in [−90, 90]422
Geometrya valid polygon, or one repaired by make_valid that is still a polygon422 “ring self-intersects” or “zero area”
Maskat least one grid cell centre inside the polygon422 “contains no grid cell centre”

A ring is evaluated as a user polygon. The mask rule is cell-centre-inside (MASK_RULE in roi.py), which differs from the v1 lattice closed rule used for the source ROIs. A user ROI is therefore METHOD_EXECUTED over a user mask and is never reported as NUMERIC_REPRODUCED or admitted.

The ring’s identity is the SHA-256 of its vertices formatted at 1e-9 degree. The first 12 hexadecimal characters label user ROIs (user:<12 hex>). If the ring digest matches a served source ring exactly, the ROI is labelled source:<index> and its kind is source-ring. Otherwise its kind is user-drawn.

The Moon’s Source ROIs control lists the reference rings served by GET /site. Each has an index roi and a closed ring of lon/lat pairs. Selecting one submits its served ring as a normal ROI, so it resolves to the source:<index> identity above.

τ is the slope-RMS cutoff. It is a data-quality threshold on the slope RMS grid, not an uncertainty, error bar or confidence bound.

For each τ, a cell is jointly valid when both of these hold:

  • the slope is finite and within [0, 90] degrees;
  • the slope RMS is finite and strictly positive.

A jointly valid cell is included when its slope RMS is less than or equal to τ (inclusive):

included(τ)={ c:valid(sc)∧valid(σc)∧σc≤τ }\text{included}(\tau) = \{\, c : \text{valid}(s_c) \land \text{valid}(\sigma_c) \land \sigma_c \le \tau \,\}

The inclusion fraction is included_cells / joint_cells. When joint_cells is zero the fraction is undefined (null), not zero. The default τ values are 0.5, 2.0 and 25.0.

Selecting a row in the Rows table selects that τ in the research context.

Brushing links a single grid cell between the Moon and the Cells grid. It runs in both directions as a ResearchCommand.

  1. With an evaluated ROI selected and not in drawing mode, click the Moon.
  2. The browser sends the lon/lat pick, the ROI ring and the current τ values to GET /cell.
  3. Python projects the pick, checks that it lies inside the ROI, finds the containing pixel and returns that cell’s values and per-τ inclusion.
  4. The linked cell centre is marked on the Moon, and the Cells grid scrolls to and highlights the matching sampled row. If the cell is not in the stride sample, the panel shows its Python values with the note “not in the sample”.

Clicking a sampled cell links its Python coordinates (row, col, and the cell-centre lon_deg and lat_deg from the sample) to the Moon marker, through the cell.link command.

Python inverts the grid affine transform to find the containing pixel:

  • The pick is projected from lon/lat into the grid CRS.
  • The pixel coordinates (column, row) are the inverse affine transform of the projected point.
  • A pixel’s footprint is half-open in grid coordinates: the top and left edges are included, and the bottom and right edges are excluded. Concretely, the cell is row = floor(pixel_row), col = floor(pixel_col), and the cell centre is the affine transform of (col + 0.5, row + 0.5).
  • Grid bounds are 0 ≤ pixel_row < rows and 0 ≤ pixel_col < cols.

ROI membership is tested at the picked point, not at the cell centre. A pick can therefore be inside the ROI while its containing cell centre lies outside the sampling mask. The response is then a valid “not in sample” result, not an error.

  • Non-finite slope or slope RMS serialises as null, never as zero.
  • included is a map keyed by Python’s τ labels ("0.5", "2", "25", formatted as %g).
  • Each cell request is decoded with Effect Schema before it reaches research state.
ConditionHTTP statusBody
Pick is inside the Site grid and, when ring is supplied, inside that ring200Cell result
Pick is outside the Site grid404{"error": "location is outside the Site grid"}
Pick is outside the supplied ROI404{"error": "location is outside the selected ROI"}
Longitude or latitude is missing, non-numeric, non-finite, or latitude is outside [−90, 90]422{"error": "..."}
ring is not a JSON list, or fails the ring checks422{"error": "..."}
tau values are not finite and non-negative422{"error": "..."}

The grid check precedes the ROI check, so a point outside both returns the grid message.

ROI changes, τ changes and newer sample clicks abort any pending cell request. A late response therefore cannot replace the current linked cell.

The workbench and cell tests, the native localhost sidecar with the Effect decoder, the commands, and a research-state smoke test have been run without a Vite server. Cesium marker rendering and the live Perspective highlight have not yet been observed in a browser capture. See Residuals.