-
-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: add martin integration #649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
mashazyu
wants to merge
12
commits into
dev
Choose a base branch
from
feat/add-martin-integration
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 3 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
6a9e36c
chore: setup martin and add analysis_view
mashazyu 367534d
fix: port routing and add endpoint to expose view data
mashazyu 9d6d49d
refactor: remove unnecessary documentation
mashazyu b940d3e
feat: add markAsReadParam to clear view once data is read
mashazyu 3d8b2ce
feat: convert measurements from jsonb to columns in analysis_view
mashazyu 5476268
doc: udpate martin_setup.md
mashazyu d9bbb0d
chore: remove endpoints and update martin setup doc
mashazyu a201950
fix: return api.measurements.ts deleted by mistake
mashazyu 2a7ceb5
doc: highlight that it's a local setup
mashazyu 63db8aa
fix: remove last line from api.measurements.ts
mashazyu 9d81dc4
fix: one more
mashazyu 3fffcf0
feat: convert analysis_view to regular view to allow automatic refres…
mashazyu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| # Martin Tile Server Setup | ||
|
|
||
| This document explains how to use Martin tile server to serve vector tiles from your PostGIS database. | ||
|
|
||
| ## Overview | ||
|
|
||
| Martin is a tile server that generates and serves vector tiles on the fly from PostGIS databases. It's configured to: | ||
| - Run in the same Docker network as your PostGIS database | ||
| - Automatically discover tables and views with geometry columns (SRID 4326) | ||
| - Serve them as Mapbox Vector Tiles | ||
|
|
||
| ## Setup | ||
|
|
||
| ### 1. Database Views | ||
|
|
||
| **Analysis View**: A database view `analysis_view` has been created for data analysis. The migration is in `drizzle/0023_create_analysis_view.sql`. | ||
|
|
||
| The view provides a denormalized structure with: | ||
| - `createdAt`: measurement timestamp | ||
| - `boxId`: device ID | ||
| - `tags`: device tags array | ||
| - `geometry`: location point (SRID 4326) | ||
| - `measurements`: JSONB object containing all sensor measurements | ||
|
|
||
| **Note**: Martin automatically discovers tables and views with geometry columns that have SRID 4326. The `analysis_view` geometry column is properly configured for Martin. | ||
|
|
||
| ### 2. Docker Configuration | ||
|
|
||
| Martin is configured in `docker-compose.yml`: | ||
| - Runs on port `3001` (host) mapped to port `3000` (container) | ||
| - Connects to the database specified in your `DATABASE_URL` (defaults to `opensensemap`) | ||
| - Uses the same Docker network (`app-network`) | ||
| - Waits for Postgres to be healthy before starting | ||
|
|
||
| ### 3. Environment Variables | ||
|
|
||
| Add to your `.env` file (optional): | ||
| ```bash | ||
| MARTIN_URL=http://localhost:3001 | ||
| ``` | ||
|
|
||
| If not set, it defaults to `http://localhost:3001`. | ||
|
|
||
| **Note**: Martin runs on port `3001` to avoid conflicts with the frontend dev server (port `3000`). | ||
|
|
||
| **Note**: Martin's `DATABASE_URL` in `docker-compose.yml` should match your application's database name. Currently configured for `opensensemap` database. | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Accessing Martin | ||
|
|
||
| Martin is accessible directly at `http://localhost:3001` (or your configured `MARTIN_URL`): | ||
|
|
||
| - **TileJSON**: `http://localhost:3001/{source_name}` | ||
| - Returns metadata about the tile source | ||
| - Example: `http://localhost:3001/analysis_view` | ||
|
|
||
| - **Tiles**: `http://localhost:3001/{source_name}/{z}/{x}/{y}.pbf` | ||
| - Returns vector tile data for a specific tile | ||
| - Example: `http://localhost:3001/analysis_view/10/512/512.pbf` | ||
|
|
||
| ## Starting the Services | ||
|
|
||
| 1. **Start Docker services**: | ||
| ```bash | ||
| docker-compose up -d | ||
| ``` | ||
| This will start both Postgres and Martin. Martin will wait for Postgres to be healthy. | ||
|
|
||
| 2. **Ensure PostGIS is enabled**: | ||
| ```bash | ||
| docker exec frontend-postgres-1 psql -U postgres -d opensensemap -c "CREATE EXTENSION IF NOT EXISTS postgis CASCADE;" | ||
| ``` | ||
|
|
||
| 3. **Run migrations** (if not already done): | ||
| ```bash | ||
| npx tsx ./db/migrate.ts | ||
| ``` | ||
| This will create the `analysis_view` and other database structures. | ||
|
|
||
| 4. **Verify Martin is running and discovering views**: | ||
| ```bash | ||
| curl http://localhost:3001/catalog | ||
| ``` | ||
| Should return JSON with available tile sources in the `tiles` object. You should see `analysis_view` listed if it has data with geometry. | ||
|
|
||
| To check if the view is properly configured: | ||
| ```bash | ||
| docker exec frontend-postgres-1 psql -U postgres -d opensensemap -c "SELECT Find_SRID('public', 'analysis_view', 'geometry');" | ||
| ``` | ||
| Should return `4326`. | ||
|
|
||
| 5. **Start the frontend**: | ||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| ## View Structure | ||
|
|
||
| The `analysis_view` groups measurements by time and device, aggregating all sensor measurements into a JSONB object: | ||
|
|
||
| ```sql | ||
| SELECT | ||
| "createdAt", -- timestamp | ||
| "boxId", -- device ID | ||
| tags, -- device tags array | ||
| geometry, -- PostGIS Point (SRID 4326) | ||
| measurements -- JSONB: { "sensor_name": { "value": ..., "unit": ..., "sensor_id": ... } } | ||
| FROM analysis_view; | ||
| ``` | ||
|
|
||
| Each sensor measurement in the `measurements` JSONB object contains: | ||
| - `value`: The measurement value | ||
| - `unit`: The unit of measurement | ||
| - `sensor_id`: The sensor ID | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import { type LoaderFunctionArgs } from "react-router"; | ||
| import { drizzleClient } from "~/db.server"; | ||
| import { sql } from "drizzle-orm"; | ||
|
|
||
| export async function loader({ request }: LoaderFunctionArgs) { | ||
| try { | ||
| const url = new URL(request.url); | ||
| const limitParam = url.searchParams.get("limit"); | ||
| const offsetParam = url.searchParams.get("offset"); | ||
| const boxIdParam = url.searchParams.get("boxId"); | ||
| const hasGeometryParam = url.searchParams.get("hasGeometry"); | ||
|
|
||
| const limit = limitParam ? Math.min(parseInt(limitParam, 10), 1000) : 100; | ||
| const offset = offsetParam ? parseInt(offsetParam, 10) : 0; | ||
| const hasGeometry = hasGeometryParam?.toLowerCase() === "true"; | ||
|
|
||
| let query = sql` | ||
| SELECT | ||
| "createdAt", | ||
| "boxId", | ||
| tags, | ||
| CASE | ||
| WHEN geometry IS NOT NULL | ||
| THEN ST_AsGeoJSON(geometry)::jsonb | ||
| ELSE NULL | ||
| END as geometry, | ||
| measurements | ||
| FROM analysis_view | ||
| WHERE 1=1 | ||
| `; | ||
|
|
||
| if (boxIdParam) { | ||
| query = sql`${query} AND "boxId" = ${boxIdParam}`; | ||
| } | ||
|
|
||
| if (hasGeometry) { | ||
| query = sql`${query} AND geometry IS NOT NULL`; | ||
| } | ||
|
|
||
| query = sql`${query} ORDER BY "createdAt" DESC LIMIT ${limit} OFFSET ${offset}`; | ||
|
|
||
| const results = await drizzleClient.execute(query); | ||
|
|
||
| // Get total count for pagination | ||
| let countQuery = sql`SELECT COUNT(*) as total FROM analysis_view WHERE 1=1`; | ||
| if (boxIdParam) { | ||
| countQuery = sql`${countQuery} AND "boxId" = ${boxIdParam}`; | ||
| } | ||
| if (hasGeometry) { | ||
| countQuery = sql`${countQuery} AND geometry IS NOT NULL`; | ||
| } | ||
| const [countResult] = await drizzleClient.execute(countQuery); | ||
| const total = Number(countResult.total); | ||
|
|
||
| return Response.json( | ||
| { | ||
| data: results, | ||
| pagination: { | ||
| limit, | ||
| offset, | ||
| total, | ||
| hasMore: offset + limit < total, | ||
| }, | ||
| }, | ||
| { | ||
| status: 200, | ||
| headers: { | ||
| "Content-Type": "application/json; charset=utf-8", | ||
| }, | ||
| }, | ||
| ); | ||
| } catch (e) { | ||
| console.warn(e); | ||
| return Response.json( | ||
| { | ||
| error: "Internal Server Error", | ||
| message: | ||
| "The server was unable to complete your request. Please try again later.", | ||
| }, | ||
| { | ||
| status: 500, | ||
| headers: { | ||
| "Content-Type": "application/json; charset=utf-8", | ||
| }, | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| -- Create an analysis view that flattens measurements with device, location, and sensor data | ||
| -- This view provides a denormalized structure for data analysis | ||
| -- | ||
| -- Structure: | ||
| -- - createdAt: measurement timestamp | ||
| -- - boxId: device ID | ||
| -- - tags: device tags array | ||
| -- - geometry: location point (SRID 4326) - uses the location from measurement if available | ||
| -- - measurements: JSONB object containing all sensor measurements | ||
| -- Each sensor is a key with value, unit, and sensor_id metadata | ||
| -- | ||
| -- Note: Groups measurements by time and device. If multiple locations exist for the same | ||
| -- time/device, uses the location from the first measurement with a location. | ||
| CREATE OR REPLACE VIEW analysis_view AS | ||
| WITH grouped_measurements AS ( | ||
| SELECT | ||
| m.time, | ||
| d.id AS device_id, | ||
| d.tags, | ||
| MAX(m.location_id) AS location_id, | ||
| -- JSONB object for all sensor measurements | ||
| -- Key: sensor_wiki_phenomenon or sensor_type or title or sensor_id | ||
| -- Value: object with value, unit, and sensor_id | ||
| COALESCE( | ||
| jsonb_object_agg( | ||
| COALESCE( | ||
| NULLIF(s.sensor_wiki_phenomenon, ''), | ||
| NULLIF(s.sensor_type, ''), | ||
| NULLIF(s.title, ''), | ||
| s.id::text | ||
| ), | ||
| jsonb_build_object( | ||
| 'value', m.value, | ||
| 'unit', COALESCE(s.unit, ''), | ||
| 'sensor_id', s.id | ||
| ) | ||
| ), | ||
| '{}'::jsonb | ||
| ) AS measurements | ||
| FROM measurement m | ||
| INNER JOIN sensor s ON m.sensor_id = s.id | ||
| INNER JOIN device d ON s.device_id = d.id | ||
| GROUP BY | ||
| m.time, | ||
| d.id, | ||
| d.tags | ||
| ) | ||
| SELECT | ||
| gm.time AS "createdAt", | ||
| gm.device_id AS "boxId", | ||
| gm.tags, | ||
| l.location::geometry(Point, 4326) AS geometry, | ||
| gm.measurements | ||
| FROM grouped_measurements gm | ||
| LEFT JOIN location l ON gm.location_id = l.id; | ||
|
|
||
| -- Add comment to help identify this view | ||
| COMMENT ON VIEW analysis_view IS 'Denormalized view for data analysis combining measurements, devices, sensors, and locations. All sensor measurements are stored in a JSONB object with value, unit, and sensor_id metadata.'; | ||
|
|
||
| -- Create index on the view's key columns for better query performance | ||
| -- Note: You may want to add indexes on the underlying tables instead: | ||
| -- CREATE INDEX idx_measurement_time ON measurement(time); | ||
| -- CREATE INDEX idx_measurement_location_id ON measurement(location_id); | ||
| -- CREATE INDEX idx_sensor_device_id ON sensor(device_id); | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: I am not sure it needs to be commited