Data Models
The main object shapes the API returns. Timestamps are epoch milliseconds unless noted.
Marker
Section titled “Marker”interface Marker { id: string; latitude: number; longitude: number; title: string; description?: string; // Markdown color: string; // hex, e.g. "#3B82F6" markerIconId?: string; // reference into the room icon registry markerIconSize?: number; // per-marker size override (px) tags?: string[]; createdBy: string; createdAt: number; eventDate?: number; // for the timeline imageKey?: string; // storage key — fetch via .../markers/image?key=… thumbnailKey?: string; imageBlob?: string; // legacy inline image, superseded by imageKey thumbnailBlob?: string; // legacy embeddedMediaUrl?: string; // YouTube / Vimeo / external mediaSize?: 'sm' | 'md' | 'lg'; // embedded-media popup width (default 'md') imageMetadata?: ImageMetadata; // EXIF read from the attached photo — read-only}
interface ImageMetadata { gpsSource: boolean; // true when the marker sits where the photo was taken cameraMake?: string; cameraModel?: string; dateTaken?: number; // epoch ms altitude?: number; orientation?: number;}imageMetadata is derived when a photo is attached in the app — a geotagged photo moves the
marker to its GPS position and sets gpsSource. It is not settable over the API, and swapping
imageKey/thumbnailKey clears it so a new photo never inherits the previous one’s
provenance.
The single-marker endpoints (GET/PATCH /api/rooms/:roomId/markers/:markerId) omit
createdByEmail and the inline blob fields (imageBlob, thumbnailBlob, iconBlob) from
their responses; the list endpoint returns the stored shape as-is.
PolygonLayer
Section titled “PolygonLayer”interface PolygonLayer { id: string; name: string; description?: string; geometryType?: string; // "Polygon" | "MultiPolygon" | "LineString" | ... color: string; opacity: number; // 0–1 strokeWeight: number; // px visible: boolean; totalFeatureCount?: number; createdBy: string; createdAt: number; eventDate?: number; bbox?: [number, number, number, number]; // [minLon, minLat, maxLon, maxLat]}RasterLayer
Section titled “RasterLayer”interface RasterLayer { id: string; name: string; description?: string; fileSize: number; // original TIFF size, bytes bounds: [number, number, number, number]; minZoom: number; maxZoom: number; opacity: number; // 0–1 visible: boolean; createdAt: number; eventDate?: number; processingStatus?: 'pending' | 'processing' | 'complete' | 'error'; // display (see the Raster Layers reference) colormap?: string; vmin?: number; vmax?: number; band?: number; // 0 = natural color / all bands renderMode?: 'tiles' | 'image_overlay' | 'rgb'; rgbBands?: [number, number, number]; // 1-based [R, G, B]}MarkerIcon
Section titled “MarkerIcon”interface MarkerIcon { id: string; name: string; iconBlob: string; // image data (base64) category?: string; size: number; // display height in px (default 27) createdAt: number;}Room & RoomMember
Section titled “Room & RoomMember”interface Room { roomId: string; createdBy: string; // owner's user ID createdAt: number; markerCount: number; commitCount: number; lastActivity?: number; isPublic?: boolean;}
interface RoomMember { userId: string; email: string; name?: string; role: 'owner' | 'editor' | 'viewer'; addedBy: string; addedAt: number;}
