7.2 KiB
7.2 KiB
Map Models
Overview
The Map module provides building-level and unit-level location management with multi-provider geocoding, volunteer shift scheduling, GeoJSON polygon cuts for map filtering, and comprehensive audit trails.
Models (7):
- Location — Building-level with lat/lng, NAR integration
- Address — Unit-level with support levels
- LocationHistory — Audit trail (7 action types)
- Shift — Volunteer shifts with cut relation
- ShiftSignup — Signup tracking
- Cut — GeoJSON polygon overlays
- MapSettings — Singleton configuration
Key Features:
- Building vs unit architecture (1 Location → many Addresses)
- Multi-provider geocoding (6 providers: Google, Mapbox, Nominatim, Photon, LocationIQ, ArcGIS)
- NAR 2025 Canadian electoral data import
- Spatial indexing (latitude/longitude composite index)
- GeoJSON polygon storage for cuts
- Walk sheet generation with QR codes
- CSV import/export
See Schema Reference for complete field listings.
Building vs Unit Architecture
Location = Building-level data:
- Single lat/lng coordinate
- Street address (no unit number)
- Building type (SINGLE_FAMILY, MULTI_UNIT, MIXED_USE, COMMERCIAL)
- Total units count
- Building notes (access codes, manager contact)
Address = Unit-level data:
- Unit number (apartment #, suite #)
- Occupant name/email/phone
- Support level (1-4)
- Sign request flag
- Canvassing notes
Relationship: Location ||--o{ Address (one-to-many)
Example:
// 1 Location: 123 Main St (4-unit apartment building)
const location = {
address: '123 Main St',
latitude: 53.5461,
longitude: -113.4938,
buildingType: 'MULTI_UNIT',
totalUnits: 4,
};
// 4 Addresses: Units 101-104
const addresses = [
{ locationId, unitNumber: '101', firstName: 'Alice', supportLevel: '4' },
{ locationId, unitNumber: '102', firstName: 'Bob', supportLevel: '3' },
{ locationId, unitNumber: '103', firstName: 'Carol', supportLevel: '2' },
{ locationId, unitNumber: '104', firstName: 'Dave', supportLevel: '1' },
];
Geocoding Providers
enum GeocodeProvider {
GOOGLE // Google Maps Geocoding API
MAPBOX // Mapbox Geocoding API
NOMINATIM // OpenStreetMap Nominatim
PHOTON // Photon (OSM-based)
LOCATIONIQ // LocationIQ (OSM-based)
ARCGIS // ArcGIS Geocoding Service
UNKNOWN // Manually entered or unknown source
}
Provider Priority:
- Google (highest accuracy, paid)
- Mapbox (high accuracy, paid)
- ArcGIS (high accuracy, free tier)
- Nominatim (medium accuracy, free)
- Photon (medium accuracy, free)
- LocationIQ (medium accuracy, free tier)
Confidence Score: 0-100 (stored in geocodeConfidence field)
NAR 2025 Import
NAR = National Address Register (Canadian electoral data)
Import Features:
- Streams large CSV files (no memory limit)
- Joins Location + Address files on LOC_GUID
- Converts BG_X/BG_Y (EPSG:3347 Lambert projection) → lat/lng
- Province selector (codes 10-62)
- City/postal/cut filters
- Residential-only toggle (buildingUse = 1)
New Location Fields:
postalCode— Canadian postal codeprovince— Province code (e.g., "AB")federalDistrict— Federal electoral districtbuildingUse— NAR BU_USE (1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown)locGuid— NAR LOC_GUID (unique)
New Address Fields:
addrGuid— NAR ADDR_GUID (unique)
LocationHistory Actions
enum LocationHistoryAction {
CREATED // Location created
UPDATED // Location updated
GEOCODED // Single location geocoded
BULK_GEOCODED // Batch geocode operation
MOVED_ON_MAP // Dragged on admin map
IMPORTED_CSV // CSV import
IMPORTED_NAR // NAR import
}
Audit Fields:
field— Which field changed (e.g., "latitude")oldValue— Previous valuenewValue— New valuemetadata— JSON with provider, confidence, etc.
Cut GeoJSON Storage
Cut stores GeoJSON polygon coordinates:
{
"type": "Polygon",
"coordinates": [
[
[-113.5, 53.5],
[-113.4, 53.5],
[-113.4, 53.6],
[-113.5, 53.6],
[-113.5, 53.5]
]
]
}
Bounds: Calculated bounding box for quick filtering:
{
"north": 53.6,
"south": 53.5,
"east": -113.4,
"west": -113.5
}
Shift Status Workflow
stateDiagram-v2
[*] --> OPEN : Create shift
OPEN --> FULL : currentVolunteers >= maxVolunteers
OPEN --> CANCELLED : Admin cancels
FULL --> OPEN : Volunteer cancels (currentVolunteers < maxVolunteers)
FULL --> CANCELLED : Admin cancels
CANCELLED --> [*]
Common Queries
Create Location with Geocoding
const location = await prisma.location.create({
data: {
address: '123 Main St, Edmonton, AB',
latitude: 53.5461,
longitude: -113.4938,
geocodeProvider: GeocodeProvider.GOOGLE,
geocodeConfidence: 95,
buildingType: BuildingType.SINGLE_FAMILY,
totalUnits: 1,
createdByUserId: user.id,
history: {
create: {
userId: user.id,
action: LocationHistoryAction.GEOCODED,
metadata: { provider: 'google', confidence: 95 },
},
},
},
});
Find Locations in Bounding Box
const locations = await prisma.location.findMany({
where: {
latitude: { gte: 53.5, lte: 53.6 },
longitude: { gte: -113.5, lte: -113.4 },
},
include: { addresses: true },
});
Create Shift with Cut
const shift = await prisma.shift.create({
data: {
title: 'Weekend Canvassing - Downtown',
date: new Date('2025-02-15'),
startTime: '10:00',
endTime: '14:00',
maxVolunteers: 10,
isPublic: true,
cutId: cut.id, // Assign to cut
},
});
Public Shift Signup (Creates TEMP User)
// 1. Create TEMP user with random password
const tempPassword = generatePassword(); // "SwiftEagle42"
const tempUser = await prisma.user.create({
data: {
email: 'volunteer@example.com',
password: await bcrypt.hash(tempPassword, 10),
name: 'Jane Volunteer',
role: UserRole.TEMP,
createdVia: UserCreatedVia.PUBLIC_SHIFT_SIGNUP,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
expireDays: 30,
},
});
// 2. Create shift signup
await prisma.shiftSignup.create({
data: {
shiftId: shift.id,
userId: tempUser.id,
userEmail: 'volunteer@example.com',
userName: 'Jane Volunteer',
signupSource: SignupSource.PUBLIC,
},
});
// 3. Send confirmation email with temp password
await emailService.send({
template: 'shift-signup-confirmation',
variables: {
USER_NAME: 'Jane Volunteer',
SHIFT_TITLE: shift.title,
IS_NEW_USER: 'true',
TEMP_PASSWORD: tempPassword,
// ...
},
});
Related Documentation
- Schema Reference — Complete field listings
- Database Overview — ER diagram
- API Map Routes — REST endpoints
- Admin Locations Page — Location management UI
- Admin Cuts Page — Cut editor UI
- Public Map Page — Public map UI