This repository has been archived by the owner on Oct 6, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Node/JS backend for a geospatial visualisation system for the heavy construction industry. Real world commercial codebase I architected and was a lead dev on (and retain ownership of). All rights reserved.
License
alexkorban/csl-node
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Setup on Heroku --------------------------------------------- Setup Postgres extension: run heroku pg:psql and "create extension postgis;" in there Clone the local database with: PGUSER=alex PGPASSWORD=alex heroku pg:push safetyfirst_development HEROKU_POSTGRESQL_TEAL --app safetyfirstnode-staging Set search path in heroku pg:psql: - Run `heroku pg:credentials DATABASE_URL -a csl-safesitenode` to get the name of the database - Run this in psql: alter database <DB name, e.g. df9bvjqtn3i2oh> set search_path = '$user', 'public', 'postgis'; - Restart the psql session, and the search path can be checked with: show search_path; Setup DATABASE_URL env variable for Heroku app, this is required for node-db-migrate to connect to the database: heroku config:set DATABASE_URL=`heroku config:get HEROKU_POSTGRESQL_TEAL_URL` Migrations can be run pretty much normally: heroku run node db-migrate -e prod -v up Fork the database ------------------------------------------------------------------------------------------------------------------------ After forking an application/database, it's necessary to update the search path (see setup instructions). Restore locally from a database dump ------------------------------------------------------------------------------------------------------------------------ createdb safesite_dev psql -d safesite_dev # open the DB in psql In psql: create schema postgis; alter database safesite_dev set search_path = '$user', 'postgis', 'public'; \q Back on the command line: cd /usr/local/Cellar/postgis/2.1.4_1/share/postgis psql -d safesite_dev -f postgis.sql psql -d safesite_dev -f postgis_comments.sql psql -d safesite_dev -f spatial_ref_sys.sql In psql: alter database safesite_dev set search_path = '$user', 'public', 'postgis'; cd <dump location> pg_restore --verbose --no-acl --no-owner -d safesite_dev 2015-08-30.dump Create a project ------------------------------------------------------------------------------------------------------------------------ $ tools/create_project staging "Blake''s Crossing" 1 Australia/Adelaide polygon ../kml/Blake\'s\ crossing/boundary.json Debug SQL queries ------------------------------------------------------------------------------------------------------------------------ When a query has CTEs (intermediate sub-selects before the main select) or can be refactored into such form, then it's possible to see the output of intermediate steps: - Create the debug table: create table debug_table (id serial, t text, r text); - Add a new CTE statement into the query such as: debug_a as (insert into debug_table(t,r) select 'vis_int', row(visit_intervals.*)::text from visit_intervals), This will dump all the data from the specified sub-table into debug_table. Note that modifying CTEs (like the debug one) have to be at the top level, which means this only works with the `query` method of the DB client (not with `jsonQuery` or `jsonArrayQuery`). Importing LendLease KMLs into the DB as GeoJSON features ------------------------------------------------------------------------------------------------------------------------ Run these commands: iconv -f UTF-16 -t UTF-8 source_cutfill.kml > cutfill.kml togeojson cutfill.kml > cutfill.json Then in psql: \set content `cat cutfill.json` select j->>'type' as type, j->'properties'->>'Name' as name, j->'geometry' as g from (select json_array_elements(:'content'::json->'features') as j) as sub; insert into geometries(overlay_id, properties, geometry) select ?? as overlay_id, ('{"name":"' || (j->'properties'->>'name') || '"}')::json as properties, ST_SetSRID(ST_MakePolygon(ST_Force2D(ST_GeomFromGeoJSON(j->>'geometry'))), 4326) as geometry from (select json_array_elements(:'content'::json->'features') as j) as sub where j->'properties'->>'name' ilike '%cut%'; Downloading LendLease drone imagery ------------------------------------------------------------------------------------------------------------------------ Download with 10 parallel connections and resume: aria2c --file-allocation=none -c -x 10 -s 10 -d . <url> caffeinate -w <aria pid> Visualising positions from Heroku ------------------------------------------------------------------------------------------------------------------------ psql <conn string from heroku pg:credentials DATABASE_URL> -t -c "select st_astext(st_point(lon, lat)) from positions where user_id = 361 and created_at > '2013-05-12 20:00:00' order by created_at desc" | geojsonify | geojsonio Simplify KML geometries ------------------------------------------------------------------------------------------------------------------------ ogr2ogr -simplify 0.000001 -f KML simplified.kml original.kml Useful SQL queries ------------------------------------------------------------------------------------------------------------------------ == Set download boundaries ------------------------------------------------------------ update projects set download_boundary = (select st_simplify(st_buffer(geometry::geography, 1000)::geometry, 0.005) from geometries join overlays on overlay_id = overlays.id where (overlays.properties->>'is_boundary')::bool and project_id = projects.id) , updated_at = clock_timestamp() + '1 minute'::interval where id = 29; == Extract positions as (almost correct) GeoJSON: ------------------------------------------------------------ - Execute this in RubyMine: with features as (with coords as (select id, 'Point' as type, ARRAY[lon, lat] as coordinates from positions where user_id = 74), props as (select id, accuracy, speed, created_at, recorded_at from positions where user_id = 74) select 'Feature' as type, row_to_json(coords.*) as geometry, row_to_json(props.*) as properties from coords join props on coords.id = props.id) select '"FeatureCollection"' as type, json_agg(row_to_json(features.*)) as features from features - Copy to clipboard using the JSON extractor - the data will be correct but wrongly wrapped in square brackets == Create a project manually using a JSON boundary in a file: ------------------------------------------------------------ -- boundary.json contains a FeatureCollection (e.g. as output by togeojson when converting from KML) \set bound `cat ../kml/Dunedin/boundary.json` begin; with var as (select 'Dunedin' as project_name, 'TT5AA5' as project_cid, 99 as tpl_boundary_id, ST_SetSRID(ST_MakePolygon(ST_Force2D(ST_GeomFromGeoJSON(:'bound'::json#>>'{features,0,geometry}'))), 4326) as geometry ) , project as ( insert into projects (name, cid, download_boundary) select var.project_name, var.project_cid, -- set download boundary to be ~1km away from the project boundary st_simplify(st_buffer(var.geometry::geography, 1000)::geometry, 0.005) from var returning id, download_boundary ) , boundary as ( insert into overlays (name, project_id, display_order, properties) select 'Boundary', project.id, 100, (select properties from overlays where id = var.tpl_boundary_id) from var cross join project returning id ) , geom as ( insert into geometries (overlay_id, properties, geometry) select boundary.id, '{"name": "Project boundary"}'::json, var.geometry from boundary cross join var ) select * from geom; == Insert a geometry using a KML fragment: ------------------------------------------------------------ begin; insert into geometries (overlay_id, properties, geometry) values (98, '{"name": "Clearing line"}'::json, ST_SetSRID(ST_Force2D(ST_GeomFromKML('<Polygon><outerBoundaryIs><LinearRing><coordinates>151.2010508495358,-33.89454214039346,0 151.2010244030993,-33.89550086321904,0 151.2019655764939,-33.89559574476716,0 151.2029649224187,-33.89571585462653,0 151.2033830872738,-33.89562535128356,0 151.2035801360156,-33.89469774959951,0 151.2026081804326,-33.8947415646257,0 151.2026543751532,-33.89431695006977,0 151.2018223789218,-33.89423260397754,0 151.2013299266485,-33.89431101594254,0 151.2010508495358,-33.89454214039346,0</coordinates></LinearRing></outerBoundaryIs></Polygon>')), 4326)); == Insert a marker: ------------------------------------------------------------ insert into geometries (geometry, properties, overlay_id) values (ST_SetSRID(ST_GeomFromGeoJSON('{"type":"Point","coordinates":[174.859505,-41.129325153]}'), 4326), to_json('{"name": "JHA Station 1", "icon": "info"}'), 3); == Get restricted_areas entries in a readable form: ------------------------------------------------------------ select properties, ST_AsGeoJSON(geometry) from geometries where overlay_id = 3; == Format UTC time for the client: ------------------------------------------------------------ to_char(site_overlay_files.updated_at, 'YYYY-MM-DD HH24:MI:SSZ') as updated_at == Get a convex hull: ------------------------------------------------------------ insert into geometries (overlay_id, properties, geometry) values (57, '{}'::json, (select ST_Buffer(ST_ConcaveHull(geometry, 0.99), 0.001) from geometries where overlay_id = 7)); == Check for duplicate event records ------------------------------------------------------------ with pairs as ( select id, user_id, geometry_id, created_at, type, lead(type, 1) over (partition by user_id, project_id, date_trunc('day', created_at at time zone 'Australia/NSW'), geometry_id order by created_at rows between current row and 1 following) as pair_type from events order by user_id ) select id, user_id, geometry_id, created_at from pairs where type = pair_type order by user_id; == Get project activity stats ------------------------------------------------------------ select name, count(distinct user_id) as users, count(events)/2 as area_visits, last(events.created_at order by events.created_at) at time zone 'Australia/NSW' as latest_activity from events join projects on project_id = projects.id group by name; == Check for event positions outside download boundaries ------------------------------------------------------------ select * from events where position is not null and (select count(*) from projects where ST_Contains(download_boundary, ST_SetSRID(ST_Point((events.position->>'lon')::double precision, (events.position->>'lat')::double precision), 4326))) = 0 order by user_id, created_at; == Get platform & device information ------------------------------------------------------------ select user_id, last(properties->>'platform' order by created_at) as platform, last(properties->>'device' order by created_at) as device, last(properties->>'api_version' order by created_at) as api from info_events where type = 'app_start' group by user_id order by platform, device; == Get paving scan summary ------------------------------------------------------------ select properties->>'docket_id', string_agg(properties->>'step', ',' order by created_at) from events where type = 'concrete_movement' and project_id = 17 and created_at > now() - '5 hours'::interval group by properties->>'docket_id' order by properties->>'docket_id' desc; == Export query results to CSV ------------------------------------------------------------ \copy (select * from positions where user_id = 974 order by created_at) to '974.csv' with csv header == Set a JSON property ------------------------------------------------------------ update users_hq set permissions = json_object_set_key(permissions, 'drawing', true) where id = 1; == Detect possible stuck positions ------------------------------------------------------------ This is a query for Android users, and count > 100 filter assumes high frequency positions: with u as (select distinct user_id as id from info_events where user_id in (select distinct user_id from events where project_id = 17) and type = 'app_start' and created_at > now() - interval '60 days' and properties->>'platform' ilike 'android%') select count(*) as cnt, user_id, date_trunc('hour', created_at) as hour, lon, lat, accuracy , altitude, altitude_accuracy, speed, heading from positions where user_id in (select id from u) and created_at > now() - interval '60 days' group by user_id, hour, lon, lat, accuracy, altitude, altitude_accuracy, speed, heading having count(*) > 100 order by hour desc; False positives are possible so it's still necessary to inspect the positions manually. == Get user stats for the last 30 days ------------------------------------------------------------ with last_synced_at as ( select user_id, max(synced_at) as at from users_projects group by user_id ) , records as ( select user_id, last(properties->>'platform' order by created_at) as platform, last(properties->>'device' order by created_at) as device, last(properties->>'api_version' order by created_at) as api from info_events where type = 'app_start' group by user_id ) , grouped_records as ( select records.user_id, platform, device, api, at from records left join last_synced_at on records.user_id = last_synced_at.user_id where at >= now() - '30 days'::interval ) select 'Active mobile users today' as name, count(*)::text as value from grouped_records where at at time zone 'Australia/NSW' >= date_trunc('day', now() at time zone 'Australia/NSW') union select 'Active mobile users (30 days)', count(*)::text from grouped_records union select 'API versions in use', string_agg(api::text, ', ' order by api) from (select distinct api from grouped_records) a union select 'iOS versions', string_agg(platform, ', ' order by platform) from (select distinct platform from grouped_records where platform ilike 'ios%') b union select 'Android versions', string_agg(platform, ', ' order by platform) from (select distinct platform from grouped_records where platform ilike 'android%') c union select 'Mobile errors today', count(*)::text from info_events where type = 'error' and date_trunc('day', now() at time zone 'Australia/NSW') = date_trunc('day', created_at at time zone 'Australia/NSW') union -- Times have to be "at time zone 'Australia/NSW'" once sessions table is fixed select 'Active HQ users today', count(*)::text from sessions where date_trunc('day', now()) = date_trunc('day', updated_at) union -- Times have to be "at time zone 'Australia/NSW'" once sessions table is fixed select 'Active HQ users (30 days)', count(distinct data->>'userId')::text from sessions where updated_at >= now() - '30 days'::interval order by name == Get speed stats ------------------------------------------------------------ with t1 as (select created_at at time zone 'NZ' as local, ST_Distance(ST_SetSRID(ST_Point(lon, lat), 4326)::geography, ST_SetSRID(ST_Point(lag(lon) over(order by created_at), lag(lat) over(order by created_at)), 4326)::geography) as dist, ST_Distance(ST_SetSRID(ST_Point(lon, lat), 4326)::geography, ST_SetSRID(ST_Point(first(lon) over(partition by extract(day from created_at at time zone 'NZ') order by created_at), first(lat) over(partition by extract(day from created_at at time zone 'NZ') order by created_at)), 4326)::geography) as dist2, speed, accel, heading, lon, lat, accuracy, created_at from positions where user_id = 995 and extract(hour from created_at at time zone 'NZ') between 5 and 8 and created_at > now() - interval '7 hours' ) select local, dist, dist2, speed, accel, extract(epoch from created_at - lag(created_at) over (partition by extract(day from created_at at time zone 'NZ') order by created_at)) as tm, dist / extract(epoch from created_at - lag(created_at) over (partition by extract(day from created_at at time zone 'NZ') order by created_at)) as calc_speed, heading, lon, lat, accuracy, dist - accuracy as diff from t1 order by created_at desc == Get acceleration stats ------------------------------------------------------------ with accel as (select abs((accel->>'x')::double precision) as x, abs((accel->>'y')::double precision) as y, abs((accel->>'z')::double precision) as z, speed, lag(speed, 1) over (order by created_at) as prev_speed1, lag(speed, 2) over (order by created_at) as prev_speed2, lag(speed, 3) over (order by created_at) as prev_speed3, lead(speed, 1) over (order by created_at) as next_speed1, lead(speed, 1) over (order by created_at) as next_speed2, created_at from positions where user_id = 1002 and created_at > '2016-05-26 20:03:00' and created_at < '2016-05-27 03:26:05') , accel2 as ( select *, sqrt(x*x+y*y+z*z) as mag, greatest(x, y, z) as axmax from accel --where speed < 0.01 --and prev_speed = 0 and next_speed > 0 ) , accel3 as ( select *, ntile(10) over (order by mag) as ptile, (mag + lag(mag, 1) over (order by created_at) + lag(mag, 2) over (order by created_at)) / 3 as avg_mag, greatest(axmax + lag(axmax, 1) over (order by created_at) + lag(axmax, 2) over (order by created_at)) as axmax_3 from accel2 ) , accel4 as ( select *, ntile(10) over (order by axmax_3) as ptile2 from accel3 where -- Stop error --speed > 0.2 and ((next_speed2 < 0.01 and next_speed1 > 0.01 and prev_speed1 < 0.01) or (next_speed1 < 0.01 and prev_speed1 > 0.01 and prev_speed2 < 0.01)) -- Move start speed > 0.01 and prev_speed1 > 0.01 and prev_speed2 > 0.01 and prev_speed3 < 0.01 ) -- select min(mag), max(mag), avg(mag), min(ptile), max(ptile) -- from accel4 -- select * from accel4 select ptile2, min(axmax_3), max(axmax_3), avg(axmax_3) from accel4 group by ptile2 order by ptile2 API v4 time sending format server: `extract(epoch from <timestamp column>)::int as time` --> number of seconds client: `moment.tz(time * 1000, timeZone)` --> moment at project's time zone API v4 timestamps scheme ------------------------------------------------------------------------------------------------------------------------ ---------------------------------------------------------------------------------------------------------- File | Function/param | P/C | Producer/Consumer | Zone | Format ---------|----------------------------------|-----|---------------------------|---------|---------- weather | created_at | <-- | Meteo data | UTC | timestamp | created_at | --> | node user.getWeather | | ---------|----------------------------------|-----|---------------------------|---------|---------- user | recordInfoEvent, | <-- | mobile.Uplink | mobile | moment json | recordPosition, | | | | | recordEvent, | | | | | recordObservation, | | | | | recordVehicle, created_at | | | | | recordVehicle, rego_Exp_date | <-- | mobile, prestartChecklist | mobile | moment | getUpdatedPosition | --> | mobile | project | 'DD/MM/YY HH24:MI:SS' | getWeather | --> | mobile, weather icon | UTC | timestamp | getUpdatedCollections, | | | | | created_at | --> | mobile, collections | project | 'DD/MM/YY HH24:MI:SS' | getUpdatedVehicle, rego_exp_date | --> | mobile, vehiclesCollection| mobile | timestamp ---------|----------------------------------|-----|---------------------------|---------|----------- reports | getProjectVisits timestamp | --> | | | seconds | date | --> | | | 'DD/MM/YY HH24:MI:SS' | arrived_at | --> | | | 'HH24:MI' | departed_at | --> | | | 'HH24:MI' | getArea date | --> | | none | 'DD/MM/YY HH24:MI:SS' | duration | --> | | project | seconds | getSpeedBands duration | --> | | project | seconds | getTimeline timestamp | --> | | | seconds | created_at | --> | | | 'DD/MM/YY HH24:MI:SS' | getConcreteTests as_load_time | --> | | project | 'DD/MM/YY HH24:MI:SS' | as_test_time | --> | | project | 'DD/MM/YY HH24:MI:SS' | as_dump_time | --> | | project | 'DD/MM/YY HH24:MI:SS' | getBreaks day | --> | | project | 'DD/MM/YY HH24:MI:SS' | day_start_utc | --> | reports, getBreaks | utc | seconds | day_end_utc | --> | reports, getBreaks | utc | seconds | start | --> | | project | 'DD/MM/YY HH24:MI:SS' | end | --> | | project | 'DD/MM/YY HH24:MI:SS' | coord | --> | | | msc ---------|----------------------------------|-----|---------------------------|---------|------------ project | getBaseLayers updated_at | --> | BaseLayer.addDroneImage | project | 'DD/MM/YY HH24:MI:SS' | getUsers last_position_at| --> | UserWatcher | project | 'DD/MM/YY HH24:MI:SS' | getPositions last_position_at| --> | UserWatcher | project | 'DD/MM/YY HH24:MI:SS' | created_at | --> | | project | 'DD/MM/YY HH24:MI:SS' | getPositionTimeline timestamp | --> | | | msc | next_timestamp | --> | | | msc | getTimelineInfo start | --> | | | msc | finish | --> | | | msc ---------|----------------------------------|-----|---------------------------|---------|------------ paving | getBatchPlantInfo | --> | | project | 'HH24:MI' | getSlump hour | --> | | project | "hour" | getProduction hour | --> | | project | "hour" |
About
Node/JS backend for a geospatial visualisation system for the heavy construction industry. Real world commercial codebase I architected and was a lead dev on (and retain ownership of). All rights reserved.