From 43e98d93b84a7d687d56c1a80339af666917b6be Mon Sep 17 00:00:00 2001 From: Chris Dalke Date: Mon, 15 Jun 2026 08:55:26 -0400 Subject: [PATCH 1/2] wip --- config.toml | 20 +++-- content/crowdsourced-data/_index.md | 41 ++++++++++ .../getting-started/streaming-events.md | 58 ++++++++++++++ .../getting-started/submitting-user-data.md | 41 ++++++++++ .../user-report-api-reference/_index.md | 46 ++++++++++++ .../user-report-api-reference/create.md | 75 +++++++++++++++++++ .../user-report-api-reference/delete.md | 53 +++++++++++++ .../user-report-api-reference/list.md | 62 +++++++++++++++ .../user-report-api-reference/replication.md | 58 ++++++++++++++ .../user-report-api-reference/tile-geojson.md | 59 +++++++++++++++ .../user-report-api-reference/vote.md | 63 ++++++++++++++++ content/oem/api-reference/_index.md | 2 + .../layouts/partials/headerSecondary.html | 6 +- .../layouts/partials/leftSidebar.html | 17 +++-- 14 files changed, 588 insertions(+), 13 deletions(-) create mode 100644 content/crowdsourced-data/_index.md create mode 100644 content/crowdsourced-data/getting-started/streaming-events.md create mode 100644 content/crowdsourced-data/getting-started/submitting-user-data.md create mode 100644 content/crowdsourced-data/user-report-api-reference/_index.md create mode 100644 content/crowdsourced-data/user-report-api-reference/create.md create mode 100644 content/crowdsourced-data/user-report-api-reference/delete.md create mode 100644 content/crowdsourced-data/user-report-api-reference/list.md create mode 100644 content/crowdsourced-data/user-report-api-reference/replication.md create mode 100644 content/crowdsourced-data/user-report-api-reference/tile-geojson.md create mode 100644 content/crowdsourced-data/user-report-api-reference/vote.md diff --git a/config.toml b/config.toml index 159a301..ed3e6c9 100644 --- a/config.toml +++ b/config.toml @@ -21,12 +21,12 @@ unsafe = true weight = 1 [[menu.main]] identifier = "api_reference" - title = "Core API Reference" + title = "Core API" pre = "" weight = 2 [[menu.main]] identifier = "chart_api_reference" - title = "Chart API Reference" + title = "Chart API" pre = "" weight = 3 [[menu.main]] @@ -41,16 +41,26 @@ unsafe = true weight = 1 [[menu.oem]] identifier = "portal_api_reference" - title = "Vector Charts Portal" + title = "Admin API" pre = "" weight = 2 [[menu.oem]] identifier = "api_reference" - title = "Vector Charts Core" + title = "Core API" pre = "" weight = 3 [[menu.oem]] identifier = "examples" title = "Examples" pre = "" - weight = 3 \ No newline at end of file + weight = 4 + [[menu.crowdsourced]] + identifier = "getting_started" + title = "Getting Started" + pre = "" + weight = 1 + [[menu.crowdsourced]] + identifier = "user_report_api_reference" + title = "User Report API" + pre = "" + weight = 2 \ No newline at end of file diff --git a/content/crowdsourced-data/_index.md b/content/crowdsourced-data/_index.md new file mode 100644 index 0000000..da4b9f4 --- /dev/null +++ b/content/crowdsourced-data/_index.md @@ -0,0 +1,41 @@ +--- +title: "Crowdsourced Data API" +weight: 1 +menu: + crowdsourced: + title: "Overview" + parent: "getting_started" + weight: 1 +--- + +Crowdsourced data lets end users contribute location-based observations that other users can discover, validate, and act on. Vector Charts currently supports this through **user reports**: spatially-defined suggestions about chart data submitted at a specific latitude and longitude. + +User reports are available on both **Vector Charts Cloud** and **Vector Charts OEM**. The REST API and WebSocket event format are the same on both platforms; only the base URL differs. + +Each deployment is configured with a **namespace** (default `public`). All reports created on that instance belong to its namespace. When replicating between OEM peers, only reports in matching namespaces are exchanged. + +## Base URLs + +| Platform | Base URL | +|----------|----------| +| Cloud | `https://api.vectorcharts.com` | +| OEM | `https://:9909` | + +All paths in the [User Report API](/crowdsourced-data/user-report-api-reference/) are relative to the base URL for your deployment. + +## What You Can Build + +- Let users flag hazards, data quality issues, incidents, closures, or SOS situations on the chart +- Display nearby reports as GeoJSON tile overlays on a slippy map +- Let users vote reports up or down to surface consensus +- Receive streaming updates when reports are created, updated, deleted, or expired + +## Next Steps + +- [Submitting User Data](/crowdsourced-data/getting-started/submitting-user-data/) — integration patterns for client applications +- [Streaming Events](/crowdsourced-data/getting-started/streaming-events/) — WebSocket event format +- [User Report API](/crowdsourced-data/user-report-api-reference/) — endpoint reference + +## Authentication + +User report endpoints require a valid API token on both Cloud and OEM. See [Cloud authentication](/api-reference/api-authentication/) or the [OEM Core API overview](/oem/api-reference/) for how to pass tokens in headers or query parameters. diff --git a/content/crowdsourced-data/getting-started/streaming-events.md b/content/crowdsourced-data/getting-started/streaming-events.md new file mode 100644 index 0000000..c39373e --- /dev/null +++ b/content/crowdsourced-data/getting-started/streaming-events.md @@ -0,0 +1,58 @@ +--- +title: "Streaming Events" +weight: 3 +menu: + crowdsourced: + title: "Streaming Events" + parent: "getting_started" + weight: 3 +--- + +The Realtime API delivers JSON event messages over WebSocket when user reports are created, updated, deleted, or expired. This is available on both Vector Charts Cloud and Vector Charts OEM. + +## Connection + +Connect to: + +
+wss://api.vectorcharts.com/api/v1/realtime?token=<token string>
+
+ +On OEM, replace the host with your instance URL (for example, `wss://<your-host>:9909/api/v1/realtime?token=<token string>`). + +Authentication uses the same API token as REST endpoints, passed as the `token` query parameter. Unauthenticated connections are rejected. + +## Message Format + +Each message is a JSON object: + +
+{
+    "type": "event_user_report",
+    "data": {
+        "action": "created",
+        "report": {
+            "id": "550e8400-e29b-41d4-a716-446655440000",
+            "reportType": "hazard",
+            "position": { "latitude": 42.36, "longitude": -71.05 },
+            "properties": {},
+            "validVoteCount": 0,
+            "invalidVoteCount": 0,
+            "externalUserId": "app-user-abc123",
+            "createdAt": 1718380800000,
+            "updatedAt": 1718380800000,
+            "expiresAt": 1718467200000,
+            "isExpired": false,
+            "isDeleted": false
+        }
+    },
+    "eventAt": 1718380800000
+}
+
+ +- **type**: Event namespace. User report events use `event_user_report`. +- **data.action**: One of `created`, `updated`, `deleted`, or `expired`. +- **data.report**: Full report object with vote counts, matching the [User Report API](/crowdsourced-data/user-report-api-reference/) schema. +- **eventAt**: Event timestamp in milliseconds since Unix epoch. + +Future event types may use different `type` values on the same WebSocket connection. diff --git a/content/crowdsourced-data/getting-started/submitting-user-data.md b/content/crowdsourced-data/getting-started/submitting-user-data.md new file mode 100644 index 0000000..6220742 --- /dev/null +++ b/content/crowdsourced-data/getting-started/submitting-user-data.md @@ -0,0 +1,41 @@ +--- +title: "Submitting User Data" +weight: 2 +menu: + crowdsourced: + title: "Submitting User Data" + parent: "getting_started" + weight: 2 +--- + +This guide covers the typical client workflow for crowdsourced user reports on Cloud and OEM. + +## 1. Create a Report + +When a user marks a location on the chart, call [Create User Report](/crowdsourced-data/user-report-api-reference/create/) with: + +- **reportType** — one of `data_quality`, `hazard`, `incident`, `closure`, or `sos` +- **position** — `{ latitude, longitude }` in WGS84 decimal degrees +- **properties** (optional) — free-form JSON such as a description or heading +- **externalUserId** (optional) — your app's user identifier (not used for authorization) +- **id** (optional) — a client-generated UUID version 4 for offline-first sync + +The server returns the report with vote counts initialized to zero and an `expiresAt` timestamp (default TTL is 24 hours). + +## 2. Display Reports on the Map + +Fetch reports for the current viewport using [Get User Reports Tile](/crowdsourced-data/user-report-api-reference/tile-geojson/). Each slippy-map tile returns a GeoJSON FeatureCollection of active (non-deleted, non-expired) reports. + +For a global or paginated feed, use [List User Reports](/crowdsourced-data/user-report-api-reference/list/). + +## 3. Vote on Reports + +When a user confirms or disputes a report, call [Vote on User Report](/crowdsourced-data/user-report-api-reference/vote/) with `vote` set to `valid` or `invalid`. Each authenticated user may have at most one vote per report; submitting again updates the existing vote. + +## 4. Delete Reports + +The user who created a report (or an admin) can [Delete User Report](/crowdsourced-data/user-report-api-reference/delete/) to soft-delete it. Deleted reports are excluded from default list and tile queries. + +## 5. Streaming Updates + +Subscribe to [Streaming Events](/crowdsourced-data/getting-started/streaming-events/) at `/api/v1/realtime?token=` to receive `event_user_report` messages when reports change, instead of polling list or tile endpoints. diff --git a/content/crowdsourced-data/user-report-api-reference/_index.md b/content/crowdsourced-data/user-report-api-reference/_index.md new file mode 100644 index 0000000..13da203 --- /dev/null +++ b/content/crowdsourced-data/user-report-api-reference/_index.md @@ -0,0 +1,46 @@ +--- +title: "User Report API Overview" +weight: 1 +menu: + crowdsourced: + title: "Overview" + parent: "user_report_api_reference" + weight: 1 +--- + +Crowdsourced user reports are spatially-defined suggestions about chart data submitted by users at a specific location. Each report has a fixed type, a latitude/longitude position, optional client metadata, and vote counts aggregated from other users. + +Reports are scoped to a server-configured **namespace** (default `public`). Only reports in the configured namespace are visible via the API, included in streaming events, and replicated between peers. Peer instances must use the same namespace to exchange data. + +Reports are global within a namespace: any authenticated user can list reports, fetch tile GeoJSON, vote, and receive streaming updates. Reports expire automatically after a server-configured TTL (default 24 hours) but remain stored in the database with an `isExpired` flag. + +These endpoints are available on both [Vector Charts Cloud](https://api.vectorcharts.com) and Vector Charts OEM. Replace the host in examples with your deployment's base URL. + +## Report Types + +- `data_quality` — Data quality issue +- `hazard` — Navigation hazard +- `incident` — Incident +- `closure` — Closure or restriction +- `sos` — SOS / distress + +## Report Object + +REST endpoints return a report object with these fields: + +- **id**: UUID version 4 +- **reportType**: One of the report types above +- **position**: `{ latitude, longitude }` in WGS84 decimal degrees +- **properties**: Arbitrary JSON metadata +- **validVoteCount** / **invalidVoteCount**: Aggregated vote totals +- **externalUserId**: Optional opaque client user identifier +- **createdAt** / **updatedAt**: Milliseconds since Unix epoch +- **expiresAt**: Expiration timestamp +- **isExpired** / **isDeleted**: Lifecycle flags +- **namespace**: Namespace this report belongs to (matches server config) + +## Authentication + +All user report endpoints require a valid API token. See [Cloud authentication](/api-reference/api-authentication/) or the [OEM Core API overview](/oem/api-reference/) for header and query-parameter auth. + +For WebSocket streaming, see [Streaming Events](/crowdsourced-data/getting-started/streaming-events/). diff --git a/content/crowdsourced-data/user-report-api-reference/create.md b/content/crowdsourced-data/user-report-api-reference/create.md new file mode 100644 index 0000000..f8cc5a6 --- /dev/null +++ b/content/crowdsourced-data/user-report-api-reference/create.md @@ -0,0 +1,75 @@ +--- +title: "Create User Report" +weight: 2 +menu: + crowdsourced: + parent: "user_report_api_reference" + pre: "
POST
" +--- + +{{% apiEndpointCard method="POST" path="/api/v1/user-reports" title="Create User Report" request=`POST https://api.vectorcharts.com/api/v1/user-reports +Authorization: Bearer +Content-Type: application/json + +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "reportType": "hazard", + "position": { + "latitude": 42.36, + "longitude": -71.05 + }, + "properties": { + "description": "Shallow area reported" + }, + "externalUserId": "app-user-abc123" +}` response=`Status Code: 201 Created +Response Body: +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "reportType": "hazard", + "position": { + "latitude": 42.36, + "longitude": -71.05 + }, + "properties": { + "description": "Shallow area reported" + }, + "validVoteCount": 0, + "invalidVoteCount": 0, + "externalUserId": "app-user-abc123", + "createdAt": 1718380800000, + "updatedAt": 1718380800000, + "expiresAt": 1718467200000, + "isExpired": false, + "isDeleted": false +}` %}} + +Create a new crowdsourced user report at the given position. + +Authentication + +Requires a Bearer token in the `Authorization` header or a `token` query parameter. On OEM, use a token from your instance's [Admin API](/oem/portal-api-reference/api-tokens/). + +Base URL + +Cloud: `https://api.vectorcharts.com` — OEM: `https://<your-host>:9909` + +Request Body + +- **reportType** (Required): One of `data_quality`, `hazard`, `incident`, `closure`, or `sos`. +- **position** (Required): Object with `latitude` and `longitude` (WGS84 decimal degrees). +- **id** (Optional): Client-supplied UUID version 4. When omitted, the server assigns one. Useful for offline-first clients that pre-generate IDs before sync. +- **properties** (Optional): Arbitrary JSON metadata (for example, a description or heading). +- **externalUserId** (Optional): Opaque user identifier from your application. Not used for authorization. + +Response Schema + +Returns a [User Report](/crowdsourced-data/user-report-api-reference/) object with vote counts set to zero. + +Error Responses + +- **400 Bad Request**: Invalid `reportType`, position, or `id` is not a valid UUIDv4. +- **401 Unauthorized**: Token is missing or invalid. +- **409 Conflict**: A report with the supplied `id` already exists. + +{{% /apiEndpointCard %}} diff --git a/content/crowdsourced-data/user-report-api-reference/delete.md b/content/crowdsourced-data/user-report-api-reference/delete.md new file mode 100644 index 0000000..3e20d7b --- /dev/null +++ b/content/crowdsourced-data/user-report-api-reference/delete.md @@ -0,0 +1,53 @@ +--- +title: "Delete User Report" +weight: 6 +menu: + crowdsourced: + parent: "user_report_api_reference" + pre: "
DELETE
" +--- + +{{% apiEndpointCard method="DELETE" path="/api/v1/user-reports/{id}" title="Delete User Report" request=`DELETE https://api.vectorcharts.com/api/v1/user-reports/550e8400-e29b-41d4-a716-446655440000 +Authorization: Bearer ` response=`Status Code: 200 OK +Response Body: +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "reportType": "hazard", + "position": { + "latitude": 42.36, + "longitude": -71.05 + }, + "properties": {}, + "validVoteCount": 3, + "invalidVoteCount": 1, + "externalUserId": "app-user-abc123", + "createdAt": 1718380800000, + "updatedAt": 1718381100000, + "expiresAt": 1718467200000, + "isExpired": false, + "isDeleted": true +}` %}} + +Soft-delete a user report. The report remains in the database but is marked `isDeleted: true` and excluded from default list and tile queries. + +Authentication + +Requires a Bearer token. Only the user who created the report or an admin may delete it. + +Path Parameters + +- **id**: UUID of the report to delete. + +Response + +Returns the soft-deleted [User Report](/crowdsourced-data/user-report-api-reference/) object. + +Error Responses + +- **400 Bad Request**: Invalid report id. +- **401 Unauthorized**: Token is missing or invalid. +- **403 Forbidden**: Caller is not the report owner or an admin. +- **404 Not Found**: Report does not exist. +- **410 Gone**: Report has already been deleted. + +{{% /apiEndpointCard %}} diff --git a/content/crowdsourced-data/user-report-api-reference/list.md b/content/crowdsourced-data/user-report-api-reference/list.md new file mode 100644 index 0000000..f69bfbb --- /dev/null +++ b/content/crowdsourced-data/user-report-api-reference/list.md @@ -0,0 +1,62 @@ +--- +title: "List User Reports" +weight: 3 +menu: + crowdsourced: + parent: "user_report_api_reference" + pre: "
GET
" +--- + +{{% apiEndpointCard method="GET" path="/api/v1/user-reports" title="List User Reports" request=`GET https://api.vectorcharts.com/api/v1/user-reports?limit=50&offset=0 +Authorization: Bearer ` response=`Status Code: 200 OK +Response Body: +{ + "reports": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "reportType": "hazard", + "position": { + "latitude": 42.36, + "longitude": -71.05 + }, + "properties": {}, + "validVoteCount": 3, + "invalidVoteCount": 1, + "externalUserId": "app-user-abc123", + "createdAt": 1718380800000, + "updatedAt": 1718380900000, + "expiresAt": 1718467200000, + "isExpired": false, + "isDeleted": false + } + ], + "total": 1, + "limit": 50, + "offset": 0 +}` %}} + +List all user reports globally, ordered most recent first. Each report includes aggregated vote counts. + +Authentication + +Requires a Bearer token in the `Authorization` header or a `token` query parameter. + +Query Parameters + +- **limit** (Optional): Maximum number of reports to return. Defaults to `50`. Capped at `200`. +- **offset** (Optional): Number of reports to skip. Defaults to `0`. +- **includeDeleted** (Optional): If `true`, include soft-deleted reports. Defaults to `false`. +- **includeExpired** (Optional): If `true`, include expired reports. Defaults to `false`. + +Response Schema + +- **reports**: Array of [User Report](/crowdsourced-data/user-report-api-reference/) objects. +- **total**: Total number of reports matching the filter. +- **limit**: Applied page size. +- **offset**: Applied offset. + +Error Responses + +- **401 Unauthorized**: Token is missing or invalid. + +{{% /apiEndpointCard %}} diff --git a/content/crowdsourced-data/user-report-api-reference/replication.md b/content/crowdsourced-data/user-report-api-reference/replication.md new file mode 100644 index 0000000..0ae7e21 --- /dev/null +++ b/content/crowdsourced-data/user-report-api-reference/replication.md @@ -0,0 +1,58 @@ +--- +title: "Peer Replication" +draft: true +--- + +User reports can be replicated across multiple OEM instances using a pull-only peer sync. Each instance periodically fetches reports and votes that changed since its last successful sync from configured peer URLs. + +Bidirectional replication requires peers to be configured on both sides. Peers only exchange reports in the same **namespace** — set `userReports.namespace` to the same value on each instance that should sync. + +## Configuration + +In `default.yml` (or environment overrides): + +```yaml +userReports: + namespace: 'public' + replication: + enabled: true + tickIntervalMs: 60000 + token: 'a1b2c3d4-e5f6-4789-a012-3456789abcde' + peers: + - name: peer-east + url: https://peer-east.example.com + - name: peer-west + url: https://peer-west.example.com +``` + +- **enabled**: When false, replication tick is disabled and the internal export endpoint returns 404. +- **tickIntervalMs**: How often to pull from peers (default 60 seconds). +- **token**: Shared UUIDv4 secret. Must match on all peers. +- **peers**: List of remote instances to pull from. +- **namespace**: Namespace to scope reports on this instance (default `public`). Only data in this namespace is exported, imported, or visible via the public API. + +Environment variables: `USER_REPORTS_NAMESPACE`, `USER_REPORTS_REPLICATION_ENABLED`, `USER_REPORTS_REPLICATION_TICK_INTERVAL_MS`, `USER_REPORTS_REPLICATION_TOKEN`. + +## Internal Export Endpoint + +Peers expose changed data at: + +
+GET /api/v1/internal/user-reports/replication?since=<timestamp>
+Authorization: Bearer <replication token>
+
+ +Query parameters: + +- **since** (required): ISO 8601 timestamp or epoch milliseconds. Returns rows with `updated_at` greater than this value. +- **limit** (optional): Page size, default 500, max 2000. +- **cursor** (optional): Report UUID for pagination tie-breaking. +- **voteCursor** (optional): Vote UUID for pagination tie-breaking. + +The response includes `reports`, `votes`, `hasMore`, `nextCursor`, and `timestamp`. + +Replicated writes do not trigger WebSocket events on the receiving instance. + +## Cursor Tracking + +Each peer's last successfully ingested timestamp is stored in the `user_report_replication_cursors` table. If a peer pull fails, the cursor is not advanced and the next tick retries from the last successful position. diff --git a/content/crowdsourced-data/user-report-api-reference/tile-geojson.md b/content/crowdsourced-data/user-report-api-reference/tile-geojson.md new file mode 100644 index 0000000..8180d5b --- /dev/null +++ b/content/crowdsourced-data/user-report-api-reference/tile-geojson.md @@ -0,0 +1,59 @@ +--- +title: "Get User Reports Tile" +weight: 4 +menu: + crowdsourced: + parent: "user_report_api_reference" + pre: "
GET
" +--- + +{{% apiEndpointCard method="GET" path="/api/v1/user-reports/tiles/{z}/{x}/{y}.json" title="Get User Reports Tile" request=`GET https://api.vectorcharts.com/api/v1/user-reports/tiles/12/1240/1515.json?token=` response=`Status Code: 200 OK +Response Body: +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-71.05, 42.36] + }, + "properties": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "reportType": "hazard", + "properties": {}, + "validVoteCount": 3, + "invalidVoteCount": 1, + "externalUserId": "app-user-abc123", + "createdAt": 1718380800000, + "updatedAt": 1718380900000, + "expiresAt": 1718467200000, + "isExpired": false, + "isDeleted": false + } + } + ] +}` %}} + +Returns active (non-deleted, non-expired) user reports intersecting the given slippy-map tile as a GeoJSON FeatureCollection. + +Authentication + +Requires a Bearer token in the `Authorization` header or a `token` query parameter. + +Path Parameters + +- **z**: Tile zoom level. +- **x**: Tile column. +- **y**: Tile row. + +Response + +A GeoJSON FeatureCollection where each feature is a Point geometry. Feature `properties` include the report id, type, vote counts, timestamps, and client metadata. + +Error Responses + +- **400 Bad Request**: Invalid tile coordinates. +- **401 Unauthorized**: Token is missing or invalid. + +{{% /apiEndpointCard %}} diff --git a/content/crowdsourced-data/user-report-api-reference/vote.md b/content/crowdsourced-data/user-report-api-reference/vote.md new file mode 100644 index 0000000..368ff8c --- /dev/null +++ b/content/crowdsourced-data/user-report-api-reference/vote.md @@ -0,0 +1,63 @@ +--- +title: "Vote on User Report" +weight: 5 +menu: + crowdsourced: + parent: "user_report_api_reference" + pre: "
POST
" +--- + +{{% apiEndpointCard method="POST" path="/api/v1/user-reports/{id}/votes" title="Vote on User Report" request=`POST https://api.vectorcharts.com/api/v1/user-reports/550e8400-e29b-41d4-a716-446655440000/votes +Authorization: Bearer +Content-Type: application/json + +{ + "vote": "valid", + "externalUserId": "app-user-abc123" +}` response=`Status Code: 200 OK +Response Body: +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "reportType": "hazard", + "position": { + "latitude": 42.36, + "longitude": -71.05 + }, + "properties": {}, + "validVoteCount": 4, + "invalidVoteCount": 1, + "externalUserId": "app-user-abc123", + "createdAt": 1718380800000, + "updatedAt": 1718381000000, + "expiresAt": 1718467200000, + "isExpired": false, + "isDeleted": false +}` %}} + +Vote that a user report is valid or invalid. Each authenticated user may have at most one vote per report; submitting again updates the existing vote. + +Authentication + +Requires a Bearer token in the `Authorization` header or a `token` query parameter. + +Path Parameters + +- **id**: UUID of the report to vote on. + +Request Body + +- **vote** (Required): Either `valid` or `invalid`. +- **externalUserId** (Optional): Opaque user identifier from your application. + +Response + +Returns the updated [User Report](/crowdsourced-data/user-report-api-reference/) object with refreshed vote counts. Individual vote rows are not returned. + +Error Responses + +- **400 Bad Request**: Invalid report id or vote value. +- **401 Unauthorized**: Token is missing or invalid. +- **404 Not Found**: Report does not exist. +- **410 Gone**: Report has been deleted or expired. + +{{% /apiEndpointCard %}} diff --git a/content/oem/api-reference/_index.md b/content/oem/api-reference/_index.md index cf34c51..9f5f7f3 100644 --- a/content/oem/api-reference/_index.md +++ b/content/oem/api-reference/_index.md @@ -44,3 +44,5 @@ const map = new mapboxgl.Map({ style: "https://<your-host>:9909/api/v1/styles/base.json?token=<token string>" }); + +Crowdsourced user reports and realtime event streaming are documented in the [Crowdsourced Data](/crowdsourced-data/) section. diff --git a/themes/docs-theme/layouts/partials/headerSecondary.html b/themes/docs-theme/layouts/partials/headerSecondary.html index 2a98902..58d2b7e 100644 --- a/themes/docs-theme/layouts/partials/headerSecondary.html +++ b/themes/docs-theme/layouts/partials/headerSecondary.html @@ -5,14 +5,18 @@ {{ if eq $currentPage.Section "oem" }} {{ $menu = .Site.Menus.oem }} {{ $menuName = "oem" }} + {{ else if eq $currentPage.Section "crowdsourced-data" }} + {{ $menu = .Site.Menus.crowdsourced }} + {{ $menuName = "crowdsourced" }} {{ end }}
diff --git a/themes/docs-theme/layouts/partials/leftSidebar.html b/themes/docs-theme/layouts/partials/leftSidebar.html index 0fc766d..d9ae88e 100644 --- a/themes/docs-theme/layouts/partials/leftSidebar.html +++ b/themes/docs-theme/layouts/partials/leftSidebar.html @@ -1,12 +1,15 @@
- {{ $currentPage := . }} - {{ $menu := .Site.Menus.main }} - {{ $menuName := "main" }} - {{ if eq $currentPage.Section "oem" }} - {{ $menu = .Site.Menus.oem }} - {{ $menuName = "oem" }} - {{ end }} + {{ $currentPage := . }} + {{ $menu := .Site.Menus.main }} + {{ $menuName := "main" }} + {{ if eq $currentPage.Section "oem" }} + {{ $menu = .Site.Menus.oem }} + {{ $menuName = "oem" }} + {{ else if eq $currentPage.Section "crowdsourced-data" }} + {{ $menu = .Site.Menus.crowdsourced }} + {{ $menuName = "crowdsourced" }} + {{ end }} {{ range $menu }} {{ if .HasChildren }} From 4bb04f7d0b77dde3e01e37bc79e790bd7498dc93 Mon Sep 17 00:00:00 2001 From: Chris Dalke Date: Mon, 15 Jun 2026 09:24:47 -0400 Subject: [PATCH 2/2] wip --- .../getting-started/streaming-events.md | 9 +++-- .../getting-started/submitting-user-data.md | 10 +++-- .../user-report-api-reference/_index.md | 17 ++++----- .../user-report-api-reference/create.md | 14 +++---- .../user-report-api-reference/delete.md | 16 ++++---- .../user-report-api-reference/list.md | 12 +++--- .../user-report-api-reference/replication.md | 2 +- .../user-report-api-reference/tile-geojson.md | 10 +++-- .../user-report-api-reference/tile-mvt.md | 37 +++++++++++++++++++ .../user-report-api-reference/vote.md | 14 +++---- 10 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 content/crowdsourced-data/user-report-api-reference/tile-mvt.md diff --git a/content/crowdsourced-data/getting-started/streaming-events.md b/content/crowdsourced-data/getting-started/streaming-events.md index c39373e..d83b411 100644 --- a/content/crowdsourced-data/getting-started/streaming-events.md +++ b/content/crowdsourced-data/getting-started/streaming-events.md @@ -34,16 +34,17 @@ Each message is a JSON object: "report": { "id": "550e8400-e29b-41d4-a716-446655440000", "reportType": "hazard", - "position": { "latitude": 42.36, "longitude": -71.05 }, + "latitude": 42.36, + "longitude": -71.05, "properties": {}, "validVoteCount": 0, "invalidVoteCount": 0, "externalUserId": "app-user-abc123", + "namespace": "public", "createdAt": 1718380800000, "updatedAt": 1718380800000, - "expiresAt": 1718467200000, - "isExpired": false, - "isDeleted": false + "expiredAt": null, + "deletedAt": null } }, "eventAt": 1718380800000 diff --git a/content/crowdsourced-data/getting-started/submitting-user-data.md b/content/crowdsourced-data/getting-started/submitting-user-data.md index 6220742..68058bb 100644 --- a/content/crowdsourced-data/getting-started/submitting-user-data.md +++ b/content/crowdsourced-data/getting-started/submitting-user-data.md @@ -14,17 +14,19 @@ This guide covers the typical client workflow for crowdsourced user reports on C When a user marks a location on the chart, call [Create User Report](/crowdsourced-data/user-report-api-reference/create/) with: -- **reportType** — one of `data_quality`, `hazard`, `incident`, `closure`, or `sos` -- **position** — `{ latitude, longitude }` in WGS84 decimal degrees +- **reportType** — category string (for example `hazard`) +- **position** — `{ latitude, longitude }` in WGS84 decimal degrees (request body only) - **properties** (optional) — free-form JSON such as a description or heading - **externalUserId** (optional) — your app's user identifier (not used for authorization) - **id** (optional) — a client-generated UUID version 4 for offline-first sync -The server returns the report with vote counts initialized to zero and an `expiresAt` timestamp (default TTL is 24 hours). +The server returns the report with vote counts initialized to zero. Reports expire after the configured `expirationAgeMs` (default 24 hours). ## 2. Display Reports on the Map -Fetch reports for the current viewport using [Get User Reports Tile](/crowdsourced-data/user-report-api-reference/tile-geojson/). Each slippy-map tile returns a GeoJSON FeatureCollection of active (non-deleted, non-expired) reports. +Fetch reports for the current viewport using [Get User Reports MVT Tile](/crowdsourced-data/user-report-api-reference/tile-mvt/). The vector chart style includes this source automatically and renders reports with an alert icon. Each point feature carries the full report attributes for popups and inspection. + +For GeoJSON polling without the built-in style layer, use [Get User Reports Tile (GeoJSON)](/crowdsourced-data/user-report-api-reference/tile-geojson/). For a global or paginated feed, use [List User Reports](/crowdsourced-data/user-report-api-reference/list/). diff --git a/content/crowdsourced-data/user-report-api-reference/_index.md b/content/crowdsourced-data/user-report-api-reference/_index.md index 13da203..c8ec2b9 100644 --- a/content/crowdsourced-data/user-report-api-reference/_index.md +++ b/content/crowdsourced-data/user-report-api-reference/_index.md @@ -12,33 +12,30 @@ Crowdsourced user reports are spatially-defined suggestions about chart data sub Reports are scoped to a server-configured **namespace** (default `public`). Only reports in the configured namespace are visible via the API, included in streaming events, and replicated between peers. Peer instances must use the same namespace to exchange data. -Reports are global within a namespace: any authenticated user can list reports, fetch tile GeoJSON, vote, and receive streaming updates. Reports expire automatically after a server-configured TTL (default 24 hours) but remain stored in the database with an `isExpired` flag. +Reports expire automatically after a server-configured age (default 24 hours). Expired reports have `expiredAt` set and remain in the database. These endpoints are available on both [Vector Charts Cloud](https://api.vectorcharts.com) and Vector Charts OEM. Replace the host in examples with your deployment's base URL. ## Report Types -- `data_quality` — Data quality issue -- `hazard` — Navigation hazard -- `incident` — Incident -- `closure` — Closure or restriction -- `sos` — SOS / distress +Common examples include `data_quality`, `hazard`, `incident`, `closure`, and `sos`. The server does not enforce a fixed list. ## Report Object REST endpoints return a report object with these fields: - **id**: UUID version 4 -- **reportType**: One of the report types above -- **position**: `{ latitude, longitude }` in WGS84 decimal degrees +- **reportType**: Free-form string label for the report category +- **latitude** / **longitude**: WGS84 decimal degrees - **properties**: Arbitrary JSON metadata - **validVoteCount** / **invalidVoteCount**: Aggregated vote totals - **externalUserId**: Optional opaque client user identifier - **createdAt** / **updatedAt**: Milliseconds since Unix epoch -- **expiresAt**: Expiration timestamp -- **isExpired** / **isDeleted**: Lifecycle flags +- **expiredAt** / **deletedAt**: Null when active; set when expired or soft-deleted - **namespace**: Namespace this report belongs to (matches server config) +For Mapbox vector tiles (recommended for map styles), see [Get User Reports MVT Tile](/crowdsourced-data/user-report-api-reference/tile-mvt/). + ## Authentication All user report endpoints require a valid API token. See [Cloud authentication](/api-reference/api-authentication/) or the [OEM Core API overview](/oem/api-reference/) for header and query-parameter auth. diff --git a/content/crowdsourced-data/user-report-api-reference/create.md b/content/crowdsourced-data/user-report-api-reference/create.md index f8cc5a6..6cce670 100644 --- a/content/crowdsourced-data/user-report-api-reference/create.md +++ b/content/crowdsourced-data/user-report-api-reference/create.md @@ -27,21 +27,19 @@ Response Body: { "id": "550e8400-e29b-41d4-a716-446655440000", "reportType": "hazard", - "position": { - "latitude": 42.36, - "longitude": -71.05 - }, + "latitude": 42.36, + "longitude": -71.05, "properties": { "description": "Shallow area reported" }, "validVoteCount": 0, "invalidVoteCount": 0, "externalUserId": "app-user-abc123", + "namespace": "public", "createdAt": 1718380800000, "updatedAt": 1718380800000, - "expiresAt": 1718467200000, - "isExpired": false, - "isDeleted": false + "expiredAt": null, + "deletedAt": null }` %}} Create a new crowdsourced user report at the given position. @@ -56,7 +54,7 @@ Cloud: `https://api.vectorcharts.com` — OEM: `https://<your-host>:9909` Request Body -- **reportType** (Required): One of `data_quality`, `hazard`, `incident`, `closure`, or `sos`. +- **reportType** (Required): Category string. - **position** (Required): Object with `latitude` and `longitude` (WGS84 decimal degrees). - **id** (Optional): Client-supplied UUID version 4. When omitted, the server assigns one. Useful for offline-first clients that pre-generate IDs before sync. - **properties** (Optional): Arbitrary JSON metadata (for example, a description or heading). diff --git a/content/crowdsourced-data/user-report-api-reference/delete.md b/content/crowdsourced-data/user-report-api-reference/delete.md index 3e20d7b..a11aa99 100644 --- a/content/crowdsourced-data/user-report-api-reference/delete.md +++ b/content/crowdsourced-data/user-report-api-reference/delete.md @@ -1,6 +1,6 @@ --- title: "Delete User Report" -weight: 6 +weight: 7 menu: crowdsourced: parent: "user_report_api_reference" @@ -13,22 +13,20 @@ Response Body: { "id": "550e8400-e29b-41d4-a716-446655440000", "reportType": "hazard", - "position": { - "latitude": 42.36, - "longitude": -71.05 - }, + "latitude": 42.36, + "longitude": -71.05, "properties": {}, "validVoteCount": 3, "invalidVoteCount": 1, "externalUserId": "app-user-abc123", + "namespace": "public", "createdAt": 1718380800000, "updatedAt": 1718381100000, - "expiresAt": 1718467200000, - "isExpired": false, - "isDeleted": true + "expiredAt": null, + "deletedAt": 1718381100000 }` %}} -Soft-delete a user report. The report remains in the database but is marked `isDeleted: true` and excluded from default list and tile queries. +Soft-delete a user report. The report remains in the database with `deletedAt` set and is excluded from default list and tile queries. Authentication diff --git a/content/crowdsourced-data/user-report-api-reference/list.md b/content/crowdsourced-data/user-report-api-reference/list.md index f69bfbb..73ee682 100644 --- a/content/crowdsourced-data/user-report-api-reference/list.md +++ b/content/crowdsourced-data/user-report-api-reference/list.md @@ -15,19 +15,17 @@ Response Body: { "id": "550e8400-e29b-41d4-a716-446655440000", "reportType": "hazard", - "position": { - "latitude": 42.36, - "longitude": -71.05 - }, + "latitude": 42.36, + "longitude": -71.05, "properties": {}, "validVoteCount": 3, "invalidVoteCount": 1, "externalUserId": "app-user-abc123", + "namespace": "public", "createdAt": 1718380800000, "updatedAt": 1718380900000, - "expiresAt": 1718467200000, - "isExpired": false, - "isDeleted": false + "expiredAt": null, + "deletedAt": null } ], "total": 1, diff --git a/content/crowdsourced-data/user-report-api-reference/replication.md b/content/crowdsourced-data/user-report-api-reference/replication.md index 0ae7e21..de86c60 100644 --- a/content/crowdsourced-data/user-report-api-reference/replication.md +++ b/content/crowdsourced-data/user-report-api-reference/replication.md @@ -55,4 +55,4 @@ Replicated writes do not trigger WebSocket events on the receiving instance. ## Cursor Tracking -Each peer's last successfully ingested timestamp is stored in the `user_report_replication_cursors` table. If a peer pull fails, the cursor is not advanced and the next tick retries from the last successful position. +Each peer's last successfully ingested timestamp is stored in the `user_report_replication_peers` table. If a peer pull fails, the cursor is not advanced and the next tick retries from the last successful position. diff --git a/content/crowdsourced-data/user-report-api-reference/tile-geojson.md b/content/crowdsourced-data/user-report-api-reference/tile-geojson.md index 8180d5b..8c4380c 100644 --- a/content/crowdsourced-data/user-report-api-reference/tile-geojson.md +++ b/content/crowdsourced-data/user-report-api-reference/tile-geojson.md @@ -1,5 +1,5 @@ --- -title: "Get User Reports Tile" +title: "Get User Reports Tile (GeoJSON)" weight: 4 menu: crowdsourced: @@ -21,15 +21,17 @@ Response Body: "properties": { "id": "550e8400-e29b-41d4-a716-446655440000", "reportType": "hazard", + "latitude": 42.36, + "longitude": -71.05, "properties": {}, "validVoteCount": 3, "invalidVoteCount": 1, "externalUserId": "app-user-abc123", + "namespace": "public", "createdAt": 1718380800000, "updatedAt": 1718380900000, - "expiresAt": 1718467200000, - "isExpired": false, - "isDeleted": false + "expiredAt": null, + "deletedAt": null } } ] diff --git a/content/crowdsourced-data/user-report-api-reference/tile-mvt.md b/content/crowdsourced-data/user-report-api-reference/tile-mvt.md new file mode 100644 index 0000000..b2e8297 --- /dev/null +++ b/content/crowdsourced-data/user-report-api-reference/tile-mvt.md @@ -0,0 +1,37 @@ +--- +title: "Get User Reports MVT Tile" +weight: 5 +menu: + crowdsourced: + parent: "user_report_api_reference" + pre: "
GET
" +--- + +{{% apiEndpointCard method="GET" path="/api/v1/user-reports/tiles/{z}/{x}/{y}.mvt" title="Get User Reports MVT Tile" request=`GET https://api.vectorcharts.com/api/v1/user-reports/tiles/12/1240/1515.mvt?token=` response=`Status Code: 200 OK +Content-Type: application/vnd.mapbox-vector-tile +(binary Mapbox Vector Tile)` %}} + +Returns active (non-deleted, non-expired) user reports intersecting the given slippy-map tile as a Mapbox vector tile (MVT). + +The `user_reports` source layer includes point features with report attributes: `id`, `report_type`, `properties` (JSON string), `valid_vote_count`, `invalid_vote_count`, `external_user_id`, `namespace`, `created_at`, and `updated_at`. + +Map style integration + +The vector chart style (`/api/v1/styles/base.json`) includes a `userReports` vector source pointing at this endpoint and a symbol layer that renders each report with the `DANGER01` alert icon. Query feature properties in the client to show report details on click. + +Authentication + +Requires a Bearer token in the `Authorization` header or a `token` query parameter. + +Path Parameters + +- **z**: Tile zoom level. +- **x**: Tile column. +- **y**: Tile row. + +Error Responses + +- **400 Bad Request**: Invalid tile coordinates. +- **401 Unauthorized**: Token is missing or invalid. + +{{% /apiEndpointCard %}} diff --git a/content/crowdsourced-data/user-report-api-reference/vote.md b/content/crowdsourced-data/user-report-api-reference/vote.md index 368ff8c..7e73426 100644 --- a/content/crowdsourced-data/user-report-api-reference/vote.md +++ b/content/crowdsourced-data/user-report-api-reference/vote.md @@ -1,6 +1,6 @@ --- title: "Vote on User Report" -weight: 5 +weight: 6 menu: crowdsourced: parent: "user_report_api_reference" @@ -19,19 +19,17 @@ Response Body: { "id": "550e8400-e29b-41d4-a716-446655440000", "reportType": "hazard", - "position": { - "latitude": 42.36, - "longitude": -71.05 - }, + "latitude": 42.36, + "longitude": -71.05, "properties": {}, "validVoteCount": 4, "invalidVoteCount": 1, "externalUserId": "app-user-abc123", + "namespace": "public", "createdAt": 1718380800000, "updatedAt": 1718381000000, - "expiresAt": 1718467200000, - "isExpired": false, - "isDeleted": false + "expiredAt": null, + "deletedAt": null }` %}} Vote that a user report is valid or invalid. Each authenticated user may have at most one vote per report; submitting again updates the existing vote.