Mall Floor Plans: Vector-First Architecture
Product Direction
MallGuide floor plans are vector-first.
- Shopper-facing floor maps must render from IMDF vector data only.
- Source floor images are ingest/verification artifacts, not the user map layer.
- No shopper UI should rely on image-pin overlays for map rendering.
Canonical Data Model
- Geometry (
/features)
- Units:
Feature+geometry.type=Polygon+properties.featureType=unit - Amenities/icons:
Feature+geometry.type=Point+properties.featureType=amenity - Cross-floor connectors: set
properties.connectorGroupIdon lift/escalator/stairs amenities that are the same physical shaft/run between floors. - Coordinates are normalized to
[0,1]range relative to the floor plan image. - Deleting a unit feature cascade-deletes all linked occupancies.
- Tenant identity (
/unit-occupancies)
- Link a unit polygon to a tenant via:
unitFeatureIdstoreIdand/orbrandId- optional
outletName
- At least one of
storeId,brandId, oroutletNameis required. - Only one active occupancy per unit at a time — creating a new active occupancy atomically deactivates existing ones.
- Branding/media
- Logos come from store/brand records (
store.logoURL,brand.logoURL). - Logos are rendered on vector maps via occupancy linkage, not stored in IMDF geometry.
- Floor plan image & aspect ratio
FloorData.floorPlanImageURL— the active floor plan source image.FloorData.aspectRatio— width/height ratio, computed server-side on upload viaPOST /floorplan.- Client-side fallback: loads the image to read
naturalWidth/naturalHeightifaspectRatiois not set.
- Source images (
/source-images)
- Collection of all source images for a floor (operator plans, Apple Maps, Google Maps, screenshots).
- Rich metadata: quality, coverage, orientation, distortion, AI extraction status.
- Enums defined in
@mallguide/shared:floorSourceImageKinds,floorSourceImageStatuses,floorSourceImageQualities, etc. - First uploaded source image auto-sets as the floor plan image if none exists.
- Store placement fallback
- Stores remain floor-scoped (
mallId+floorId); there is no mall-level store collection without floors. - If a store create request targets a missing floor, the API auto-creates that floor before saving the store.
- Preferred metadata fields on store create requests:
floorNamefloorLevelfloorEditorMode
- If those fields are omitted, the API creates a placeholder floor named
Auto-created floor (<floorId>). floorEditorModefalls back to the first submitted store position mode (image,grid, orlist), otherwiselist.
API Responsibilities
GET /malls/{mallId}/floors/{floorId}/vector-map- Primary read endpoint for floor plan rendering.
- Returns:
featureCollection,unitOccupancies,activeUnitOccupanciesByFeatureId,floorPlanImageURL,aspectRatio.
POST /malls/{mallId}/floors/{floorId}/floorplan- Upload floor plan image (PNG, JPEG, WebP).
- Computes and stores
aspectRatiofrom image dimensions. - Returns:
{ floorPlanImageURL, aspectRatio }.
GET/POST/PATCH/DELETE /malls/{mallId}/floors/{floorId}/source-images- Full CRUD for source images with file upload support.
- Auto-sets
floorPlanImageURLon first upload if floor has none.
GET/POST/PATCH/DELETE /malls/{mallId}/floors/{floorId}/features- IMDF feature CRUD with geometry quality gate (POST/PATCH return 422 if geometry is suspect).
GET/POST/PATCH/DELETE /malls/{mallId}/floors/{floorId}/unit-occupancies- Unit occupancy CRUD with batch endpoint (
POST .../unit-occupancies/batch).
- Unit occupancy CRUD with batch endpoint (
POST /malls/{mallId}/floors/{floorId}/storesPOST /malls/{mallId}/floors/{floorId}/stores/batch- If
floorIddoes not exist yet, the API auto-creates the floor. - The floor is seeded from request metadata when available:
floorNamefloorLevelfloorEditorMode
- Otherwise a placeholder floor document is created.
- Floor creation, store creation, and mall store-count updates are committed in one batch to avoid partial writes.
- If
Quality Gate
Feature POST and PATCH run geometry quality evaluation before persisting. If the geometry is suspect (e.g. too many placeholder labels, shell-like polygons, tiny areas), the request returns 422 with diagnostics:
{
"error": "...",
"errorCode": "FLOOR_GEOMETRY_SUSPECT",
"reasons": ["..."],
"diagnostics": ["..."],
"hints": ["..."]
}UI Responsibilities
- Mall shopper floor-plan tab renders vector map only.
- Mobile gesture contract:
- The app shell does not browser-zoom; navbar, tabs, and popups stay fixed.
- The vector floor plan owns pinch-zoom and pan internally via pointer gestures.
- Gesture start must be allowed even when a finger lands on an interactive store or amenity overlay.
- Only vector controls should block gesture start; overlays should still block viewport click-through.
- Geographic floor-frame contract:
- When floor features are in real-world coordinates, shopper rendering uses a shared mall-space frame, not a per-floor tight fit.
- Floor switching should preserve relative shop positions between floors so stacked levels line up visually.
- Each floor may also carry an optional
mapAlignmenttransform (offsetX,offsetY,rotation) applied in feature-space before rendering. mapAlignmentis per-floor, not per-mall. It exists to visually stack floors that are individually correct but slightly misregistered against each other.- Admin alignment workflow uses a ghost overlay of another floor while nudging the selected floor. The selected floor remains interactive; the reference floor is view-only.
- Use stable structural references for alignment: lift cores, escalator banks, atrium edges, corridor intersections, and slab corners. Do not align by anchor-store footprints alone.
- Source image (if present) is optional for internal verification/editing workflows.
- Admins can toggle source image preview (
off,next to vector,below vector) for QA/diagnostics. - Adding/updating icons or unit geometry must use IMDF feature APIs.
- Tenant placement must use unit occupancy APIs, not image pin coordinates.
- Walkway highlight toggle (desktop only) highlights corridor/walking space between stores.
- Floor-plan desktop sidebar includes indoor navigation controls:
- Destination search box above shop details.
- Destination picker (store + floor context).
- Route preference toggle:
either,lift,escalator.
- Shopper can place and persist current location marker; route starts from that marker.
Indoor Navigation (Current Status)
- Navigation UI is scaffolded (destination picker, route preference, sidebar).
- Route computation in
useVectorNavigation.tsis not yet implemented (TODO). - Destination anchors are derived from active unit occupancy -> store linkage.
- Lift/escalator routing uses nearest mapped amenity points:
liftpreference usesamenityCategory=elevatorescalatorpreference usesamenityCategory=escalatoreitherprefers shortest practical route and may route direct.
- When shared
connectorGroupIdvalues exist between floors, routing prefers those explicit connector links.
Logging
All floor plan API operations are logged with structured logging visible in Google Cloud Logging:
[floors]— floor CRUD, image upload, vector-map serving[features]— feature CRUD, quality gate rejections[occupancies]— occupancy CRUD, batch operations, status transitions[source-images]— source image CRUD, storage upload/cleanup, auto-set floor plan[stores]— store CRUD, duplicate skips, missing-floor auto-create
Repair Script
Existing malformed data can be repaired with:
pnpm -C functions backfill:missing-floors
pnpm -C functions backfill:missing-floors -- --apply
pnpm -C functions backfill:missing-floors -- --mall-id <mallId> --apply- Default mode is dry-run.
- The script scans store documents, detects missing floor docs, and creates placeholder floors.
- Placeholder floors use
editorModeinferred from existing store positions when possible, otherwiselist.