From 51bfb25a94863dd36a144c55c67c1fed01e6058b Mon Sep 17 00:00:00 2001 From: andrewmuratov Date: Sun, 4 Oct 2026 03:16:21 -0400 Subject: [PATCH] fix: preserve campus building selection identity Co-authored-by: Codex --- src/CampusContributionStudio.jsx | 8 ++- src/campus-contribution-data.js | 30 +++------- src/canonical-building-identity.js | 24 ++++++++ tests/campus-contribution-selection.test.mjs | 62 ++++++++++++++++++++ 4 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 src/canonical-building-identity.js create mode 100644 tests/campus-contribution-selection.test.mjs diff --git a/src/CampusContributionStudio.jsx b/src/CampusContributionStudio.jsx index b423b1f..56eeaaf 100644 --- a/src/CampusContributionStudio.jsx +++ b/src/CampusContributionStudio.jsx @@ -18,6 +18,7 @@ import { MAP_HEIGHT, MAP_WIDTH, campusFromQuery, + canonicalBuildingCodeForFeature, canonicalBuildingsForCampus, canonicalEntrancesForCampus, canonicalFootprintsForCampus, @@ -832,7 +833,8 @@ export default function CampusContributionStudio() { {canonicalFootprints.map((feature, index) => { - const code = feature.properties.buildingCode?.toUpperCase(); + const code = canonicalBuildingCodeForFeature(feature); + if (!code) return null; const key = canonicalBuildingKey(code); const selected = selectedBuildingKey === key; return ( @@ -840,7 +842,7 @@ export default function CampusContributionStudio() { key={`${code}-${index}`} d={geometryPath(feature.geometry, project)} className={selected ? 'selected' : ''} - onClick={tool === 'entrance' && !selected ? (event) => { + onClick={tool === 'entrance' && !selectedBuilding && !selected ? (event) => { event.stopPropagation(); selectBuilding(key); } : undefined} @@ -858,7 +860,7 @@ export default function CampusContributionStudio() { key={item.id} d={geometryPath(item.geometry, project)} className={selected ? 'selected' : ''} - onClick={tool === 'entrance' && !selected ? (event) => { + onClick={tool === 'entrance' && !selectedBuilding && !selected ? (event) => { event.stopPropagation(); selectBuilding(key); } : undefined} diff --git a/src/campus-contribution-data.js b/src/campus-contribution-data.js index 9a90f5c..6e4e586 100644 --- a/src/campus-contribution-data.js +++ b/src/campus-contribution-data.js @@ -23,6 +23,12 @@ import brockCampus from "../universities/brock/campus.json"; import ubcCampus from "../universities/ubc/campus.json"; import waterlooCampus from "../universities/waterloo/campus.json"; import mcgillCampus from "../universities/mcgill/campus.json"; +import { + canonicalBuildingCodeForFeature, + universityBuildingFootprint, +} from "./canonical-building-identity.js"; + +export { canonicalBuildingCodeForFeature } from "./canonical-building-identity.js"; const universityCampusModules = import.meta.glob( "../universities/**/campus.json", @@ -161,32 +167,12 @@ const TRI_CAMPUS_FOOTPRINTS = { function universityFootprints(dataset) { return (dataset?.buildings || []) .filter((b) => b.geometry) - .map((b) => ({ - type: "Feature", - id: b.id, - properties: { - buildingId: b.id, - code: b.nativeCodes?.[0] || b.id, - name: b.name, - }, - geometry: b.geometry, - })); + .map(universityBuildingFootprint); } function universityBuildings(dataset, campusId) { return (dataset?.buildings || []).map((building) => { - const feature = building.geometry - ? { - type: "Feature", - id: building.id, - properties: { - buildingId: building.id, - code: building.nativeCodes?.[0] || building.id, - name: building.name, - }, - geometry: building.geometry, - } - : null; + const feature = universityBuildingFootprint(building); const features = feature ? [feature] : []; const code = building.nativeCodes?.[0] || building.id.toUpperCase(); const entrances = (dataset.entrances || []).filter( diff --git a/src/canonical-building-identity.js b/src/canonical-building-identity.js new file mode 100644 index 0000000..73547af --- /dev/null +++ b/src/canonical-building-identity.js @@ -0,0 +1,24 @@ +export function universityBuildingFootprint(building) { + if (!building?.geometry) return null; + const buildingCode = building.nativeCodes?.[0] || String(building.id).toUpperCase(); + return { + type: "Feature", + id: building.id, + properties: { + buildingId: building.id, + buildingCode, + code: building.nativeCodes?.[0] || building.id, + name: building.name, + }, + geometry: building.geometry, + }; +} + +export function canonicalBuildingCodeForFeature(feature) { + const value = + feature?.properties?.buildingCode ?? + feature?.properties?.code ?? + feature?.properties?.buildingId ?? + feature?.id; + return value == null ? null : String(value).trim().toUpperCase() || null; +} diff --git a/tests/campus-contribution-selection.test.mjs b/tests/campus-contribution-selection.test.mjs new file mode 100644 index 0000000..613db54 --- /dev/null +++ b/tests/campus-contribution-selection.test.mjs @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { readFile } from "node:fs/promises"; + +import { + canonicalBuildingCodeForFeature, + universityBuildingFootprint, +} from "../src/canonical-building-identity.js"; + +for (const campusId of ["carleton", "queens", "harvard"]) { + test(`${campusId} footprints retain one selectable canonical building identity`, () => { + return readFile( + new URL(`../universities/${campusId}/campus.json`, import.meta.url), + "utf8", + ).then((raw) => { + const buildings = JSON.parse(raw).buildings; + const footprints = buildings + .map(universityBuildingFootprint) + .filter(Boolean); + const selectableCodes = new Set( + buildings.map((building) => + ( + building.nativeCodes?.[0] || building.id.toUpperCase() + ).toUpperCase(), + ), + ); + const footprintCodes = footprints.map(canonicalBuildingCodeForFeature); + + assert.ok( + buildings.length > 1, + `${campusId} should expose multiple selectable buildings`, + ); + assert.ok( + footprints.length > 1, + `${campusId} should expose multiple mapped footprints`, + ); + assert.equal( + footprintCodes.includes(null), + false, + `${campusId} footprints need stable IDs`, + ); + assert.ok( + new Set(footprintCodes).size > 1, + `${campusId} footprints must not collapse to one shared selection key`, + ); + for (const code of footprintCodes) { + assert.ok( + selectableCodes.has(code), + `${campusId} footprint ${code} must select a building`, + ); + } + }); + }); +} + +test("U of T buildingCode identities remain compatible", () => { + assert.equal( + canonicalBuildingCodeForFeature({ properties: { buildingCode: "ba" } }), + "BA", + ); +});