Review

Time travel on the canvas

Review reads the local SQLite journal, reconstructs the state of tracked layers at a user-chosen date T, and projects the result onto the QGIS canvas as transient overlay layers — without ever writing back. A slim canvas-anchored date bar (Google Earth style) lets the user scrub through time. It is the read-only twin of Restore: same data, opposite intent.

Vision

Restore answers the question « put the data back ». Review answers a different one: « what did the data look like at date T ». The same audit events feed both, but Review never mutates a source layer. It reconstructs the state of tracked layers at a chosen date and builds in-memory overlay layers showing that state.

The mode is designed as a continuous companion to QGIS editing: turn it on, drag the date bar, see the past. Turn it off and every trace disappears. No styles edited on source layers. No features changed. No journal rows written.

Read-only
Zero side effects on source data

Review only reads from the journal and adds memory layers prefixed __rl_lens_. The mode never calls changeGeometry, changeAttributeValue, addFeatures or deleteFeatures on a tracked layer.

Spatial-first
Filter by viewport, not by table

The cache holds events for a time range; the renderer keeps only those whose envelope intersects the current canvas extent. Pan or zoom and the visible subset recomputes in 20–80 ms per layer.

Live
Edits appear within 600 ms

The session subscribes to the edit tracker. Each new commit is debounced (600 ms) and incrementally added to the cache and the visible overlays.

Bounded
RAM and CPU budgets explicit

~2 KB per cached event, ~10 MB at 5 000 events, ~100 MB at 50k. The render worker is cancelable; a new viewport supersedes the previous one without waiting.

What Review is (and is not)

Review is a runtime mode, not a workflow. It does not produce artefacts, it does not modify history, and it does not persist anything beyond the session.

Review isReview is not
A read-only overlay showing data state at date TAn editing tool
A time-travel reconstruction of tracked layersA diff report or attribute table
A companion to Restore (same data, opposite verb)A replacement for Restore
Driven by a canvas date bar (scrub through time)An async export job
Cleared on toggle offA persistent layer style
Naming convention. Every layer Review puts in the project is prefixed __rl_lens_ followed by an 8-character UUID and a role suffix (geom_past, arrows, attr_markers). The prefix is shared with the legacy Time Lens dock so a single teardown (workflow_service._LENS_LAYER_PREFIX) cleans both. Nothing else is touched.

The four layers

Review lives across UI, orchestration, domain, and worker threads. Each layer has a single role; dependencies flow downward only.

L1 — UI surface recover_dialog (toggle, dock host) · widgets/temporal_lens_dock widgets/review_status_widget · widgets/review_segmented_switch · widgets/temporal_lens_*_map_tool L2 — Session orchestration (UI thread) core/review_session  —  cache, overlay lifecycle, viewport debounce L3 — Pure domain (zero QGIS, zero Qt) core/lens_contracts  —  NamedTuples, Enums, fetch stats core/lens_planner  —  group by entity, classify, build timelines core/wkb_envelope  —  bbox parsing & intersection (no GEOS) L4 — Background workers (QThread) widgets/review_worker  —  initial cache load (one-shot) widgets/review_render_worker  —  per-viewport feature build (cancelable) Reuses: core/event_stream_repository · core/geometry_utils · core/lens_renderer · core/journal_manager
LayerOwnsForbidden
L1 UIDock, toggle, status pill, map tools, dialog wiringDirect cache mutation, SQL reads
L2 SessionCache lifecycle, overlay creation/teardown, debounceFeature classification, geometry math
L3 DomainContracts, planner, envelope intersectionQGIS imports, Qt signals
L4 WorkersOff-UI fetch and feature buildQGIS layer mutation, project access
Pattern jumeau. The trio lens_contractslens_plannerlens_renderer mirrors the Restore trio restore_contractsrestore_plannerrestore_executor. Same separation, opposite verb.

Session start

From toggle ON to first visible overlay. The first refresh is the slowest because the entire time range is loaded into RAM in a single QThread shot; everything afterwards reuses that cache.

1. User flips the Review toggle in the dialog Status pill inserts into QGIS status bar · phase = fetch 2. recover_dialog — build LensSelection layer fingerprints · t_min/t_max · op filter · visualization mode 3. ReviewCacheWorker (QThread) event_stream_repository.fetch_events_in_zone (no bbox at start) grouped by fingerprint → {fp: [AuditEvent]} 4. review_session.start() — UI thread create 3 memory overlays per source layer · once 5. First viewport refresh canvas.extent() → ReviewRenderWorker → layer_ready signals 6. Overlays populated · status pill = active phase = idle · entity count visible in pill tooltip Subsequent pans / zooms reuse the cache (steps 5→6 only)
StepModuleCostFailure mode
1 Togglerecover_dialogO(1)Status pill insertion forced via QApplication.processEvents() for instant feedback.
2 Selectioncore/lens_contractsO(layers)Empty selection → early exit with WARNING log.
3 Fetchwidgets/review_workerO(events) on read connectionCap MAX_EVENTS_PER_RESTORE applies; truncation flagged in fetch_stats.
4 Overlay createcore/review_sessionO(layers) × 3Geometry family detected from first event; mixed types skipped with warning.
5 First renderwidgets/review_render_workerO(events in viewport)Worker cancelable on rapid extent change.
6 Displayrecover_dialogO(features)addFeatures on memory provider ≈ 5 ms per layer.

Viewport refresh

Pan or zoom in QGIS → canvas.extentsChanged → debounce 600 ms → render worker. The cache is never re-read from SQLite; only the in-RAM dict is filtered by envelope intersection.

canvas.extentsChanged → QTimer (600 ms debounce) Subsequent extents within 600 ms restart the timer review_session.refresh_viewport(bbox) Cancel any in-flight render worker · spawn a new one Per-layer (worker thread) wkb_envelope.envelope_intersects() lens_planner.plan_lens_view() Per-feature (worker thread) repair_geometry_for_render reproject_geometry_for_render layer_ready(fp, past[], arrows[], attr_markers[]) Each item = (wkb_bytes, attribute_list) — no QGIS objects UI thread: truncate + addFeatures (≈ 5 ms / layer)
Why the worker emits raw bytes. QgsGeometry is a Qt object and cannot safely cross threads. The worker emits (wkb_bytes, attributes_list); the UI thread builds QgsGeometry.fromWkb() and calls addFeatures() in one batch.

Live edits

While Review is on, the user can keep editing tracked layers. Every commit triggers the same capture pipeline as Restore (edit_trackerwrite_queue); Review listens to the tracker's committed_features signal and updates the canvas without polling SQLite.

edit_tracker.committed_features (n events) Same signal that feeds Restore · same AuditEvent shape review_session.on_new_events(events) Filter by tracked fingerprint → append to in-RAM cache Schedule debounced refresh (600 ms) Same path as a manual pan: render worker → layer_ready
End-to-end latency. Commit → visible overlay ≈ commit_capture (~50 ms) + debounce (600 ms) + render (~80 ms). The 600 ms debounce is the dominant term; it absorbs back-to-back commits during multi-feature edit sessions.

Session stop

Toggle off, status pill close button, dialog close, project switch — four entry points, one teardown path.

TriggerEffect
Toggle offCancel render worker · cancel cache worker · session.stop() · remove all __rl_lens_* overlays · clear cache · hide status pill
Status pill ×Same as toggle off · toggle widget reverts
Dialog closeSame as toggle off · purge_lens_overlays("dialog_close") as belt-and-braces guard
Project switchSame as dialog close · reset of journal connection drops the cache implicitly
No persistence. Memory layers are not stored in the project file even if the user saves. They live in QgsProject.instance() only for the duration of the session.

Snapshot mode (Mode A)

Instead of showing what changed (diff), Snapshot mode shows how things were at a user-chosen date T. A slim date bar anchored to the bottom of the QGIS canvas (Google Earth style) lets the user drag a handle or type a date. The reconstructed state is displayed as one overlay layer per source.

Zero SQL per drag
In-RAM forward replay

The temporal_snapshot_engine replays the cached events (already bounded by time range) up to T. O(N) over cache, ~1–5 ms per 5000 events. No SQLite read per date change.

800 ms debounce
Smooth slider experience

The CanvasDateBar debounces date_changed at 800 ms so rapid drags do not queue up redundant rebuilds.

1 overlay / source
Simple result

Unlike diff mode (3 layers per source), Snapshot produces exactly one memory layer per tracked layer, containing the entities that existed at T.

Event markers
Custom painted timeline

The TemporalTimelineWidget paints operation-colored markers (green INSERT, orange UPDATE, red DELETE) with magnetic snap so the user naturally finds meaningful dates.

Algorithm (forward replay until T)

Last event ≤ TResult
INSERT or UPDATEEntity exists at T → included in overlay with reconstructed geometry + attributes
DELETEEntity was removed before T → excluded (counted as n_absent)
No events ≤ TEntity not yet created → excluded (counted as n_unknown)

Pipeline

CanvasDateBar date_changed (800 ms debounce) SnapshotRebuildWorker (QThread) SnapshotOverlaySession.update() Overlay refreshed
Distinct from diff mode. Snapshot uses __rl_snap_ prefix and the layer group « Review Snapshot ». The diff-mode overlays (__rl_lens_) are unaffected. Both can coexist but are toggled separately.

The three overlays

For every tracked source layer, Review creates exactly three memory layers. They share the source CRS but live in their own provider so styles and selections do not leak.

SuffixGeometryCarriesWhat it tells the user
geom_pastSame family as source (Point / Line / Polygon)OLD geometry of every event in scopeWhere the entity was before the change.
arrowsLineStringcentroid(OLD) → centroid(NEW) for UPDATEs with geometry changeMovement vector. Length and direction tell the eye.
attr_markersPointcentroid for UPDATEs that changed attributes only« something changed but not the shape » — opens the diff panel on click.
Attributes carried by every overlay feature. entity_fp, event_id, created_at, op, user, _rl_age, _rl_class (entity classification). For mode Updates stacked (P1), two more: _rl_opacity and _rl_stack_order. The dock's diff panel reads event_id to fetch the full attribute delta from SQLite on demand.

Visualization modes

Review ships one visualization mode today. One future mode is planned.

Active
Snapshot at T

The state of every tracked layer reconstructed at one cutoff date T. One geometry per entity, overlaid via a canvas-anchored date bar (Google Earth style). Forward-replayed from cached events in RAM; zero SQL per date change.

Planned (P3)
Animation

Time-driven playback through the timeline. Replays Snapshot frames at user-controlled speed. Reuses the same overlays, only the active subset changes per tick.

Entity classification

For every entity, the planner inspects its OLD/NEW geometries against the selection bbox at first and last event. The result is one of seven classes carried in the _rl_class attribute and used by the dock symbology.

ClassMeaning
CREATED_IN_ZONEFirst event is INSERT and falls inside the bbox.
DELETED_FROM_ZONELast event is DELETE; previous geometries were inside.
MOVED_INTO_ZONEOld geometry outside, new geometry inside.
MOVED_OUT_OF_ZONEOld geometry inside, new geometry outside.
MOVED_WITHIN_ZONEBoth inside, geometry differs.
ATTR_ONLY_IN_ZONEGeometry stable, attributes changed.
UPDATED_IN_ZONEGeneric UPDATE inside the bbox; fallback bucket.

Module catalog — Contracts

Pure data, zero QGIS, zero Qt. The vocabulary every other Review module agrees on.

FileLinesOwns
core/lens_contracts.py186Enums (LensOpFilter, LensVisualizationMode, EntityClassification) and NamedTuples (LensSelection, EntityState, EntityTimeline, LensFetchStats, LensRenderPlan, LensRenderResult, LensRefreshOutcome).
core/wkb_envelope.py250WKB envelope parser using only the standard library. parse_envelope(wkb) returns a 4-tuple; envelope_intersects(a, b) answers in O(1) without GEOS.

Module catalog — Domain (planner, renderer)

Pure logic. Same import discipline as the contracts. Tested without a running QGIS instance.

FileLinesRole
core/lens_planner.py236Groups events by entity (fingerprint preferred, identity_json fallback for shapefile FID recycling), orders timelines, builds EntityStates with old/new geom and attr delta, classifies the entity. Output: LensRenderPlan.
core/lens_renderer.py549Synchronous renderer that consumes a plan and creates the three QGIS memory layers in one call. Used by the standalone Time Lens dock; reused by review_session for overlay creation primitives (_make_overlay_layer) and by the render worker as the attribute schema reference.
core/temporal_snapshot_engine.py290Snapshot mode core. Forward-replays cached events until cutoff date T. Per entity: last event ≤ T → INSERT/UPDATE = exists, DELETE = absent. Returns SnapshotResult mapping {datasource_fp: {entity_fp: SnapshotFeature}}. Pure Python, O(N) over cache, ~1–5 ms per 5000 events.
core/snapshot_overlay_session.py650Manages one memory layer per source layer showing the reconstructed state at date T. Lifecycle: start()update(snapshot_result)stop(). Prefix __rl_snap_, group « Review Snapshot ».

Module catalog — Session & workers

Where domain meets QGIS. The session lives on the UI thread; the workers run off it.

FileLinesRole
core/review_session.py505Owns the in-RAM event cache, the three overlay layer IDs per source, the active flag, and the destination CRS. Methods: start(), refresh_viewport(bbox), refresh_one_layer(fp, bbox), on_new_events(events), stop().
widgets/review_worker.py117One-shot QThread for the initial cache load. Emits progress(layer_name, n_events, idx), finished_ok(trace_id, cache), finished_err(trace_id, msg).
widgets/review_render_worker.py512Per-viewport feature builder. Cancelable. Emits layer_ready(fp, past[], arrows[], attr_markers[]) with raw (wkb_bytes, attrs) tuples and finally all_done({n_entities, n_features, elapsed_ms}).
widgets/snapshot_rebuild_worker.py319Snapshot mode worker. Per-date SQL query (debounced 800 ms by CanvasDateBar). CTE isolates MAX(created_at) per entity_fp, then inner join fetches full row. O(N entities) rows. Emits result_ready(trace_id, SnapshotResult) and markers_ready.

Module catalog — UI surface

FileLinesRole
widgets/temporal_lens_dock.py994QDockWidget hosting the Review panel: presets, custom date range, op filter, mode selector, entity list, attribute diff panel, status messages, error banner. Fully i18n (FR/EN, 56 entries).
widgets/temporal_lens_map_tool.py108Rectangle map tool used to draw the Review selection bbox.
widgets/temporal_lens_polygon_map_tool.py111Polygon map tool variant for irregular zones.
widgets/review_status_widget.py246Status bar pill: phase dot (idle / fetch / render / alert), entity counter, OFF button. Inserted in the QGIS main status bar only when a session is active.
widgets/review_segmented_switch.py114Segmented switch « Present / Review » for the dialog header.
widgets/canvas_date_bar.py410Snapshot mode UI. Slim QWidget anchored to canvas bottom (Google Earth style). Hosts QDateEdit + TemporalTimelineWidget + stats label. Debounced date_changed signal (800 ms). Purges stale bars across plugin reloads.
widgets/temporal_timeline_widget.py343Custom-painted timeline replacing QSlider. DPI-aware, operation-colored markers (INSERT green, UPDATE orange, DELETE red), adaptive ruler (hours / days / months / years), magnetic snap within 10 px, tooltip preview on hover.

Threading model

Four threads cooperate: UI, cache worker, render worker (diff mode), and snapshot rebuild worker (snapshot mode). No shared mutable state crosses the boundary except the immutable event cache snapshot passed by reference to the render worker.

UI thread recover_dialog toggle · debounce review_session cache · overlay add temporal_lens_dock entity list, diff panel Worker threads (QThread) review_worker (cache load — one-shot) review_render_worker (per refresh — cancelable) SQLite read-only connection PRAGMA query_only=1 · shared with stats & search
ThreadOwnsLifetime
UICache mutation, overlay creation, addFeatures, signal handlersPlugin lifetime
Cache workerInitial bulk SELECT into the cache dictOne shot per session start
Render workerEnvelope filter + planner + repair + reproject + WKB serializationOne per viewport refresh, cancelable

Status bar pill

The QGIS status bar carries two RecoverLand widgets with distinct roles. The Review pill is hidden when the mode is off; it appears only when a session runs.

WidgetVisible whenShows
StatusBarIndicator (existing)AlwaysTracking on/off, journal health, event count.
ReviewStatusWidget (new)Review session activePhase dot (idle / fetch / render / alert), Review · n entity counter, OFF button (×).
Insertion order. The pill is inserted with insertPermanentWidget(0, …) so it sits left of the QGIS coordinate display. QApplication.processEvents() is called right after insertion to force an immediate repaint before recover_and_load() blocks the UI thread.

Performance budget

OperationTargetMechanism
Toggle ON → pill visible< 50 msSynchronous insert + processEvents()
Cache load (10k events, 5 layers)< 1.5 sSingle bounded SELECT in QThread
Viewport refresh (one layer in extent)20–80 msPure-Python envelope filter on cached dict
UI thread addFeatures≈ 5 ms / layerMemory provider, batched in one call
Live edit → visible< 800 ms600 ms debounce + 80 ms render + 5 ms add
RAM at 50k cached events≈ 100 MB~2 KB per AuditEvent in cache
Snapshot rebuild (5k entities)1–5 msPure-Python forward replay on cached events, zero SQL
Snapshot date change → overlay< 900 ms800 ms debounce + 5 ms replay + addFeatures
Truncation. Above MAX_EVENTS_PER_RESTORE events in the time range, the fetch returns a truncated list and fetch_stats.n_events_truncated is non-zero. The dock paints a red header banner; older events are simply not displayed.

Integration with Restore & Rewind

The three modes (Restore, Rewind, Review) consume the same audit events and share four core modules. Restore and Rewind write back; Review only reads. The relationship is « same data, opposite verb ».

ModuleUsed by RestoreUsed by Review
core/event_stream_repositoryfetch_events_after_cutofffetch_events_in_zone
core/geometry_utilsWKB ⇆ QgsGeometry, comparison+ repair_geometry_for_render · reproject_geometry_for_render
core/identityMatch snapshot to live featureGroup events by entity
core/journal_managerRead/write SQLite connectionRead-only SQLite connection
Why share, not duplicate. Invariant IL-I4 from the Time Lens charter: any logic that already exists for Restore is extended in place, never copied. New Review-specific behaviour goes into lens_* or review_* modules; reusable primitives are added to the existing core modules.
Read this next. See Architecture for the full plugin map. The Capture pipeline is the upstream that feeds Review. The Restore pipeline is the symmetric write-back path.

Known debt

The mode is healthy on most axes. One debt item remains. Naming it here makes it easier to attack later.

Debt 1
Legacy render paths still present

The synchronous core/lens_renderer.execute_lens_render() and the async widgets/review_render_worker (from the former diff modes) remain in the codebase. They are no longer the primary path (Snapshot uses SnapshotRebuildWorker + SnapshotOverlaySession) but are not yet removed.

Healthy
Everything else

Contracts pure, session bounded, workers cancelable, status pill scoped to active sessions. No leak detected in lifecycle scenarios.