diff --git a/apps/mapping/firebase/utils.py b/apps/mapping/firebase/utils.py index 95304c1f..ed12c08c 100644 --- a/apps/mapping/firebase/utils.py +++ b/apps/mapping/firebase/utils.py @@ -503,7 +503,7 @@ def results_to_temp_table( # result = geojson.dumps(geojson.GeometryCollection(result)) # NOTE [Important]: - # If the result is a list (e.g. Locate Features project), + # If the result is a list (e.g. Locate Objects project), # example: result = [1, 0, 0, 1] # Each index represents a partition of the same task. # We create one row per partition using: diff --git a/apps/project/exports/mapping_results.py b/apps/project/exports/mapping_results.py index 3ba2a10f..558f349a 100644 --- a/apps/project/exports/mapping_results.py +++ b/apps/project/exports/mapping_results.py @@ -20,7 +20,7 @@ def generate_mapping_results(*, destination_filename: Path, project: Project) -> pd.DataFrame: - # NOTE: Add task_partition_index for Locate Feature. + # NOTE: Add task_partition_index for Locate Objects. task_partition_index_col = ( f"MSR.{fd_name(MappingSessionResult.task_partition_index)} as task_partition_index," if project.project_type_enum == ProjectTypeEnum.LOCATE diff --git a/apps/project/migrations/0012_alter_project_project_type.py b/apps/project/migrations/0012_alter_project_project_type.py new file mode 100644 index 00000000..ab53455c --- /dev/null +++ b/apps/project/migrations/0012_alter_project_project_type.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.5 on 2026-06-09 17:47 + +import django_choices_field.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('project', '0011_alter_project_project_type'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='project_type', + field=django_choices_field.fields.IntegerChoicesField(choices=[(1, 'Find Features'), (2, 'Validate Footprints'), (10, 'Assess Images'), (3, 'Compare Dates'), (4, 'Check Completeness'), (7, 'View Streets'), (9, 'Locate Objects')]), + ), + ] diff --git a/apps/project/models.py b/apps/project/models.py index c01312cf..12b31df4 100644 --- a/apps/project/models.py +++ b/apps/project/models.py @@ -114,7 +114,7 @@ class ProjectTypeEnum(models.IntegerChoices): STREET = 7, "View Streets" """ Street project type. """ - LOCATE = 9, "Locate Features" + LOCATE = 9, "Locate Objects" """ Locate project type. """ # TODO(thenav56): Confirm if we have more/less diff --git a/assets b/assets index 46da4563..b85c20a4 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 46da456329f7fe7ee9b764059a00266a28afc747 +Subproject commit b85c20a4e0b0f42169295421bce4faebe7b18fae diff --git a/docs/adr/0001-rename-locate-features-to-locate-objects.md b/docs/adr/0001-rename-locate-features-to-locate-objects.md new file mode 100644 index 00000000..69779fb6 --- /dev/null +++ b/docs/adr/0001-rename-locate-features-to-locate-objects.md @@ -0,0 +1,160 @@ +--- +status: accepted +date: 2026-06-09 +deciders: [MapSwipe Governance Team (Katherine Kinzer)] +--- + +# 0001. Keep Locate as a separate project type and rename it to "Locate Objects" + +## Context and Problem Statement + +MapSwipe has a long-standing `Find` project type with the display label "Find +Features" and a newer `Locate` project type — not yet released to production — +that was introduced with the label "Locate Features". While testing `Locate` on +staging, the MapSwipe governance team and other stakeholders found the two names +easy to confuse and the on-device views hard to tell apart. The resemblance is +specific: on the mobile client, `Find` presents a 2×3 grid of imagery tiles, +whereas `Locate` presents a single tile with a 2×2 / 4×4 / 8×8 sub-grid overlaid +(its `sub_grid_size`), and +that overlaid grid looks similar to — though not identical to — `Find`'s tile +layout. The two also answer different questions — `Find` records *whether* a +feature exists in a tile, while `Locate` records *where* within the tile it is — +and `Locate` uses a different tap convention, but a volunteer cannot tell any of +that apart at a glance. Both being raster tile-map-service types, they also share +the same backend primitives, which reinforced the impression that they were +near-duplicates. + +This prompted the question of whether `Locate` and `Find` should be merged into a +single project type, or whether the confusion should be resolved another way. The +discussion drew in HOT (Kshitij Sharma, implementing the feature), Toggle Corp +engineering (Ankit Mehta), and several community members, with the MapSwipe +governance team making the final call. The full thread is recorded in [MapSwipe +Slack](https://mapswipe.slack.com/archives/C0770LYH4N7/p1778240364512639). + +## Considered Options + +- **Option 1 — Merge `Locate` into `Find`.** Collapse the two into one project + type, eliminating the naming and UI overlap entirely. +- **Option 2 — Keep both, rename the `Locate` label to "Locate Objects".** + Change only the human-readable display label; leave the project type, its + enum value, and its code identifier unchanged. +- **Option 3 — Do nothing.** Keep "Locate Features" and accept the confusion. + +## Decision Outcome + +Chosen: **Option 2** — keep `Locate` and `Find` as separate project types and +rename the `Locate` display label from "Locate Features" to "Locate Objects". + +Merging (Option 1) was seriously weighed — the implementer initially favoured +shipping `Locate` as a `Find` variant that can sub-divide tiles — but was +rejected for two independent reasons. + +**The deciding factor was user interaction and data quality.** `Locate` uses a +different tap convention from `Find` (a single tap marks a single feature, a +double tap marks multiple). Overloading one project type with two interaction +models risks confusing volunteers who have used `Find` for years and degrading +the data they produce; the governance team judged this the overriding concern. + +**The engineering recommendation reinforced it:** `Locate` and `Find` differ +significantly in how a project is configured, how a task answer is shaped, and +how results are exported, so merging would force one model onto the other (or a +conditional union) and add ongoing maintenance and regression risk for no +functional gain: + +- **Result shape.** A `Locate` task answer is a *list* of values + (`FbProjectLocateMappingResult.results: dict[str, list[int]]`) — one entry per + sub-grid partition — whereas a `Find` task answer is a single scalar + (the generic `FbMappingResult.results: dict[str, int]`). There is no + Find-specific result model. +- **Project configuration.** `LocateProjectProperty` adds `sub_grid_size`, + `custom_options`, and `export_meta_key`/`export_meta_value`; + `FindProjectProperty` is an empty pass-through of the shared base and adds + none of these. +- **Export pipeline.** `Locate` exports add a `task_partition_index` column and + aggregate one row per `(task, partition)`, so a single task can yield several + output rows; `Find` exports omit that column and produce one row per task. + The two also derive their result-value `*_count`/`*_share` columns from + different option sets (Locate's configured custom options vs Find's fixed + `0/1/2/3` fallback). + +Renaming the label resolves the naming confusion at low cost — a state-only +migration plus any client-side label updates (see Consequences) — without +changing behaviour: the project type's enum value (`9`), its code identifier +(`LOCATE` / the `locate` module), the GraphQL `ProjectTypeEnum` value, and the +Firebase contract are all unchanged. Only the display label changes, which +clients resolve from the enum at GraphQL query time (the `AppEnumCollection` +query returns each project type's `key` and `label`). + +Among the names floated in the thread — keeping "Locate Features", or "Find+ +Features", "Tag Features", "Label Features", or "Locate Objects" — the governance +team chose "Locate Objects": different enough from "Find Features" to remove the +naming collision while still describing what the task does. + +This decision is already implemented in code — `apps/project/models.py` now +labels `LOCATE` as "Locate Objects", with the accompanying +`0012_alter_project_project_type.py` migration (commit `349956d`). Because +`Locate` has not yet been released to production, the rename lands before any +production data or released client depends on the old label. + +## Consequences + +- **Good:** removes the "Find Features" vs "Locate Features" collision in the + dashboards and the project-creation UI. +- **Good:** no data or behaviour change. The stored value stays `9`, so the + Firebase contract and the GraphQL enum value are unaffected and no data + migration is needed. Because `Locate` is still pre-production, the rename also + lands before any production projects or released clients depend on the old + label. +- **Cost (accepted):** the rename is cosmetic and does **not** address the + underlying resemblance in *interaction*. `Locate` keeps a `Find`-like sub-grid + view but a different tap convention, so a volunteer can still open it expecting + `Find` and tap wrongly. As raised in the discussion, a separate project type + changes only a few words on the project card, not the in-task experience — + closing that gap needs a UI/UX signal, not just a label. +- **Cost (accepted):** terminology is now mixed. The `Locate` custom-option + defaults still read "Single Feature" / "Multiple Features", and the sibling + type is still "Find Features", so "feature" language persists around a type + now branded "objects". Aligning that wording was deliberately left out of + this change. +- **Cost:** changing the label alters the field's choices, which Django captures + in migration state, so a state-only `AlterField` migration is required — it + updates the stored choice label, not the database column — and CI's + `makemigrations --check` enforces that it be committed. Separately, any client + that hardcodes the literal string "Locate Features" must be updated. +- **Revisit if:** `Locate` and `Find` converge — both on the interaction model + (taps) and on the data-structure/export — so the reasons for keeping them apart + no longer hold (for example, if `Locate` drops sub-grid partitioning and adopts + Find's tap convention), at which point merging the two types becomes viable + again; or if confusion between the two persists — in staging review now, or with + users once `Locate` ships — indicating the fix needs to reach the in-task UI + rather than just the label. + +## Pros and Cons of the Options + +### Option 1 — Merge `Locate` into `Find` + +- Good: eliminates the naming and UI overlap at the root; one fewer project type + to maintain. +- Bad: overloads one project type with two different tap conventions, risking + poor data from volunteers who expect `Find`'s interaction — the governance + team's overriding concern. +- Bad: forces the partitioned list-result model and the `sub_grid_size` / + `custom_options` / `export_meta_*` configuration onto a type that has none of + them, or introduces conditional branching across storage, the Firebase + contract, and the export pipeline. +- Bad: breaking change to an established, in-use project type and its exports. + +### Option 2 — Keep both, rename the label (chosen) + +- Good: resolves the naming confusion with a display-only change; no data, + enum, code-identifier, or Firebase changes. +- Bad: cosmetic only — leaves the mobile-UI similarity and the residual + "feature" wording unaddressed. A deeper rename (the `locate` code identifier + and the "Single Feature" / "Multiple Features" custom-option defaults) was + possible but deliberately scoped out, as it would churn code and the + Firebase/GraphQL contract for no functional gain. + +### Option 3 — Do nothing + +- Good: zero effort. +- Bad: leaves the confusion the governance team explicitly flagged unresolved.