Skip to content

Sampling, global locations, and UI improvements - #30

Merged
mberg merged 14 commits into
masterfrom
feb3-cleanup
Feb 5, 2026
Merged

Sampling, global locations, and UI improvements#30
mberg merged 14 commits into
masterfrom
feb3-cleanup

Conversation

@mberg

@mberg mberg commented Feb 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Global locations: Locations (buildings) are now global entities that associate with campaigns through spatial overlap (quadkey/pixel_area joins) rather than a direct campaign_id foreign key. Buildings imported by one campaign are automatically available to other campaigns covering the same area — no re-import needed. The coverage table retains campaign_id for campaign-specific predictions.

  • Building sampling: Added buildings_per_pixel option to round creation. After pixels are adaptively sampled, buildings within each selected pixel are sampled (default: 5 per pixel). Sampled buildings appear highlighted on the map and in the Predicted Coverage table.

  • Round creation simplification: Round creation is now a synchronous DB insert with optional per-area auto-sampling. Coverage pixels are created automatically when computing pixels for a campaign area.

  • Adaptive sampling improvements: Batch SQL for round assignment, persist sampling status on campaign areas, optimize by removing GeoJSON/sf overhead, fix border pixel handling.

  • Pixel enrichment fixes: Resolve data sources by name prefix, query pixels via pixel_area joins, skip already-enriched pixels, update cached population from pixel metadata.

  • UI improvements: Preserve map viewport when toggling map mode, reduce building fill opacity to 0.1 so outlines are prominent, add sampled/pixel count column to coverage table, move pixel quadkey label to top-left, add category colors to campaign areas, fix TacticalMultiSelect styling.

  • Tile performance: PostgreSQL tuning migration for spatial queries, optimize pixels_by_campaign tile function, fix spatial index usage.

Test plan

  • Import buildings via Overture for a campaign area
  • Verify buildings appear on map with light fill and visible outlines
  • Create a second campaign covering the same area — buildings should appear without re-import
  • Create a round with pixel + building sampling
  • Verify sampled buildings appear highlighted on the map
  • Verify sampled pixel counts are correct in campaign areas table
  • Verify Predicted Coverage table shows correct sampled/pixel counts
  • Verify map viewport persists when toggling map modes

mberg added 14 commits February 4, 2026 16:07
Round creation is now a synchronous DB insert with optional per-area
auto-sampling via CampaignAreaSamplingWorkflow. The CreateRoundModal
shows campaign area checkboxes with per-area sample counts instead of
adaptive sampling parameters.

Coverage pixels are created automatically when computing pixels for a
campaign area, so they appear in the Predicted Coverage table immediately.

Fix JSON parsing in sampling activity to handle extra R function output.
Rename round filter "All Rounds" to "Sampled Only" for clarity and
default to showing all pixels unfiltered.
Rename "Sampled Only" back to "All Rounds" - selecting any round
inherently means sampled. Default round filter to "All Rounds" when
round data exists, empty (show everything) otherwise.
…cache

- Resolve data source by name prefix when frontend sends string instead of UUID
- Fix enrichment activity to query pixels via pixel_area join (not campaign_id column)
- Skip already-enriched pixels to avoid redundant processing
- Update cached_population from pixel_metadata after enrichment completes
- Use pixel_metadata as population source in cache computation queries
After pixels are adaptively sampled, optionally select up to N buildings
per pixel using the same adaptive sampling service. Buildings share the
level-18 quadkey with their pixel. Default: 5 in CreateRoundModal, 0
(disabled) if not specified.

New activity sample_buildings_within_pixels groups locations by quadkey,
calls sampling service for pixels with more buildings than the limit,
and assigns selected buildings to the round via the coverage table.
Lift viewport state (lng/lat/zoom) to LocationsPage so it persists
when MapView unmounts/remounts during map mode toggle. Previously,
switching modes reset the map to the initial bounds.
- Add buildings_per_pixel option to cluster sampling
- Show existing building counts in AddCampaignAreaModal
- Preserve map viewport when toggling map mode
- Fix TacticalMultiSelect styling
- Optimize pixels_by_campaign migration
Locations are now global entities (like pixels and population) that
associate with campaigns through spatial overlap via quadkey/pixel_area
joins. The coverage table retains campaign_id for campaign-specific data.

- Add migration script to drop campaign_id, update constraints, recreate
  tile functions and materialized view
- Update all routes and activities to use spatial joins instead of
  campaign_id filters on locations
- Update ON CONFLICT clauses to match new coverage unique constraint
- Update building count queries to use pixel_area joins
- Update duplicate detection to work globally by external_id
@mberg mberg changed the title Feb3 cleanup Sampling, global locations, and UI improvements Feb 5, 2026
@mberg
mberg merged commit 7129ca1 into master Feb 5, 2026
3 of 4 checks passed
@mberg
mberg deleted the feb3-cleanup branch February 5, 2026 03:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8bbcaf265b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 307 to +308
LIMIT %s OFFSET %s
""", (campaign_id, batch_size, offset))
""", (campaign_id, metadata_field_name, batch_size, offset))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove OFFSET pagination from mutating enrichment query

In enrich_area_pixels, the loop pages unenriched pixels with LIMIT/OFFSET while each batch write updates pixel_metadata so those rows no longer match pm.metadata IS NULL OR NOT pm.metadata ? .... Because the filtered result set shrinks after every batch, incrementing offset skips remaining rows (for example, after enriching the first 500 rows, the next query starts at offset 500 of the now-smaller set), so many pixels are never processed even though total_pixels counted them initially.

Useful? React with 👍 / 👎.

round_data = cursor.fetchone()
round_id = str(round_data[0])

conn.commit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep round creation atomic with workflow startup failures

This handler commits the new round before starting area workflows, but still returns a 500 if run_async(start_area_workflows()) fails; in that failure path, except rolls back an already-committed transaction, so clients see "failed to create round" even though the round exists and retries can create duplicates. This produces inconsistent state whenever Temporal is unavailable or one workflow start errors.

Useful? React with 👍 / 👎.

Comment on lines +79 to +80
SELECT id FROM locations WHERE id = %s
""", (uploaded_location_id,))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope ID-based visit matching to the target campaign

The bulk visit import now accepts any existing locations.id without verifying campaign membership, so a CSV containing a valid UUID from another campaign will attach visits to that foreign location under the current campaign. This cross-campaign linkage bypasses the area-scoped proximity check and can corrupt campaign-level visit/coverage data.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant