Skip to content
This repository has been archived by the owner on Aug 9, 2021. It is now read-only.

Commit

Permalink
Add processing fields (#350)
Browse files Browse the repository at this point in the history
* added processing attributes
  • Loading branch information
GP authored Feb 22, 2021
1 parent 8a2f9e1 commit a455a3a
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 122 deletions.
113 changes: 58 additions & 55 deletions api/V1/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,37 @@ import middleware from "../middleware";
import auth from "../auth";
import models from "../../models";
import responseUtil from "./responseUtil";
import {body, param, query} from "express-validator/check";
import { body, param, query } from "express-validator/check";
import { Application } from "express";
import { Validator } from "jsonschema";

const JsonSchema = new Validator();

const validateStationsJson = (val, { req }) => {
const stations = JSON.parse(val);
const stations = JSON.parse(val);

// Validate json schema of input:
JsonSchema.validate(stations, {
type: "array",
minItems: 1,
uniqueItems: true,
items: {
properties: {
name: { type: "string" },
lat: { type: "number" },
lng: { type: "number" },
},
required: ["name", "lat", "lng"]
}
}, {throwFirst: true});
req.body.stations = stations;
return true;
// Validate json schema of input:
JsonSchema.validate(
stations,
{
type: "array",
minItems: 1,
uniqueItems: true,
items: {
properties: {
name: { type: "string" },
lat: { type: "number" },
lng: { type: "number" }
},
required: ["name", "lat", "lng"]
}
},
{ throwFirst: true }
);
req.body.stations = stations;
return true;
};


export default function (app: Application, baseUrl: string) {
const apiUrl = `${baseUrl}/groups`;

Expand Down Expand Up @@ -191,33 +194,30 @@ export default function (app: Application, baseUrl: string) {
);

/**
* @api {post} /api/v1/groups/{groupIdOrName}/stations Add, Update and retire current stations belonging to group
* @apiName PostStationsForGroup
* @apiGroup Group
* @apiDescription A group admin or an admin with globalWrite permissions can update stations for a group.
*
* @apiUse V1UserAuthorizationHeader
*
* @apiParam {Number|String} group name or group id
* @apiParam {JSON} Json array of {name: string, lat: number, lng: number}
*
* @apiUse V1ResponseSuccess
* @apiUse V1ResponseError
*/
* @api {post} /api/v1/groups/{groupIdOrName}/stations Add, Update and retire current stations belonging to group
* @apiName PostStationsForGroup
* @apiGroup Group
* @apiDescription A group admin or an admin with globalWrite permissions can update stations for a group.
*
* @apiUse V1UserAuthorizationHeader
*
* @apiParam {Number|String} group name or group id
* @apiParam {JSON} Json array of {name: string, lat: number, lng: number}
*
* @apiUse V1ResponseSuccess
* @apiUse V1ResponseError
*/
app.post(
`${apiUrl}/:groupIdOrName/stations`,
[
auth.authenticateUser,
middleware.getGroupByNameOrIdDynamic(param, "groupIdOrName"),
body("stations")
.exists()
.isJSON()
.withMessage("Expected JSON array")
.custom(validateStationsJson),
body("fromDate")
.isISO8601()
.toDate()
.optional(),
.exists()
.isJSON()
.withMessage("Expected JSON array")
.custom(validateStationsJson),
body("fromDate").isISO8601().toDate().optional()
],
middleware.requestWrapper(async (request, response) => {
const stationIds = await models.Group.addStationsToGroup(
Expand All @@ -235,27 +235,30 @@ export default function (app: Application, baseUrl: string) {
);

/**
* @api {get} /api/v1/groups/{groupIdOrName}/stations Retrieves all stations from a group, including retired ones.
* @apiName GetStationsForGroup
* @apiGroup Group
* @apiDescription A group member or an admin member with globalRead permissions can view stations that belong
* to a group.
*
* @apiUse V1UserAuthorizationHeader
*
* @apiParam {Number|String} group name or group id
*
* @apiUse V1ResponseSuccess
* @apiUse V1ResponseError
*/
* @api {get} /api/v1/groups/{groupIdOrName}/stations Retrieves all stations from a group, including retired ones.
* @apiName GetStationsForGroup
* @apiGroup Group
* @apiDescription A group member or an admin member with globalRead permissions can view stations that belong
* to a group.
*
* @apiUse V1UserAuthorizationHeader
*
* @apiParam {Number|String} group name or group id
*
* @apiUse V1ResponseSuccess
* @apiUse V1ResponseError
*/
app.get(
`${apiUrl}/:groupIdOrName/stations`,
[
auth.authenticateUser,
middleware.getGroupByNameOrIdDynamic(param, "groupIdOrName")
],
middleware.requestWrapper(async (request, response) => {
if (request.user.hasGlobalRead() || await request.user.isInGroup(request.body.group.id)) {
if (
request.user.hasGlobalRead() ||
(await request.user.isInGroup(request.body.group.id))
) {
const stations = await request.body.group.getStations();
return responseUtil.send(response, {
statusCode: 200,
Expand All @@ -265,7 +268,7 @@ export default function (app: Application, baseUrl: string) {
} else {
return responseUtil.send(response, {
statusCode: 403,
messages: ["User is not member of group, can't list stations"],
messages: ["User is not member of group, can't list stations"]
});
}
})
Expand Down
31 changes: 23 additions & 8 deletions api/V1/recordingUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
VisitSummary,
isWithinVisitInterval
} from "./Visits";
import {Station, StationId} from "../../models/Station";
import { Station, StationId } from "../../models/Station";

export interface RecordingQuery {
user: User;
Expand All @@ -66,21 +66,30 @@ export interface RecordingQuery {
export const MIN_STATION_SEPARATION_METERS = 60;
// The radius of the station is half the max distance between stations: any recording inside the radius can
// be considered to belong to that station.
export const MAX_DISTANCE_FROM_STATION_FOR_RECORDING = MIN_STATION_SEPARATION_METERS / 2;
export const MAX_DISTANCE_FROM_STATION_FOR_RECORDING =
MIN_STATION_SEPARATION_METERS / 2;

export function latLngApproxDistance(a: [number, number], b: [number, number]): number {
export function latLngApproxDistance(
a: [number, number],
b: [number, number]
): number {
const R = 6371e3;
// Using 'spherical law of cosines' from https://www.movable-type.co.uk/scripts/latlong.html
const lat1 = (a[0] * Math.PI) / 180;
const costLat1 = Math.cos(lat1);
const sinLat1 = Math.sin(lat1);
const lat2 = (b[0] * Math.PI) / 180;
const deltaLng = ((b[1] - a[1]) * Math.PI) / 180;
const part1 = Math.acos(sinLat1 * Math.sin(lat2) + costLat1 * Math.cos(lat2) * Math.cos(deltaLng));
const part1 = Math.acos(
sinLat1 * Math.sin(lat2) + costLat1 * Math.cos(lat2) * Math.cos(deltaLng)
);
return part1 * R;
}

export async function tryToMatchRecordingToStation(recording: Recording, stations?: Station[]): Promise<Station | null> {
export async function tryToMatchRecordingToStation(
recording: Recording,
stations?: Station[]
): Promise<Station | null> {
// If the recording does not yet have a location, return
if (!recording.location) {
return null;
Expand All @@ -94,10 +103,16 @@ export async function tryToMatchRecordingToStation(recording: Recording, station
const stationDistances = [];
for (const station of stations) {
// See if any stations match: Looking at the location distance between this recording and the stations.
const distanceToStation = latLngApproxDistance(station.location.coordinates, recording.location.coordinates);
stationDistances.push({distanceToStation, station});
const distanceToStation = latLngApproxDistance(
station.location.coordinates,
recording.location.coordinates
);
stationDistances.push({ distanceToStation, station });
}
const validStationDistances = stationDistances.filter(({distanceToStation}) => distanceToStation <= MAX_DISTANCE_FROM_STATION_FOR_RECORDING);
const validStationDistances = stationDistances.filter(
({ distanceToStation }) =>
distanceToStation <= MAX_DISTANCE_FROM_STATION_FOR_RECORDING
);

// There shouldn't really ever be more than one station within our threshold distance,
// since we check that stations aren't too close together when we add them. However, on the off
Expand Down
15 changes: 9 additions & 6 deletions api/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,16 @@ function getGroupByNameOrId(checkFunc: ValidationChainBuilder): RequestHandler {
);
}

function getGroupByNameOrIdDynamic(checkFunc: ValidationChainBuilder, fieldName: string): RequestHandler {
function getGroupByNameOrIdDynamic(
checkFunc: ValidationChainBuilder,
fieldName: string
): RequestHandler {
return oneOf(
[
getModelById(models.Group, fieldName, checkFunc),
getModelByName(models.Group, fieldName, checkFunc)
],
"Group doesn't exist or hasn't been specified."
[
getModelById(models.Group, fieldName, checkFunc),
getModelByName(models.Group, fieldName, checkFunc)
],
"Group doesn't exist or hasn't been specified."
);
}

Expand Down
24 changes: 20 additions & 4 deletions migrations/20210120160000-create-stations.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,21 @@ module.exports = {
type: Sequelize.GEOMETRY,
allowNull: false
},
createdAt: { type: Sequelize.DATE, allowedNull: false, defaultValue: Sequelize.NOW },
updatedAt: { type: Sequelize.DATE, allowedNull: false, defaultValue: Sequelize.NOW },
retiredAt: { type: Sequelize.DATE, allowedNull: true, defaultValue: Sequelize.NULL }
createdAt: {
type: Sequelize.DATE,
allowedNull: false,
defaultValue: Sequelize.NOW
},
updatedAt: {
type: Sequelize.DATE,
allowedNull: false,
defaultValue: Sequelize.NOW
},
retiredAt: {
type: Sequelize.DATE,
allowedNull: true,
defaultValue: Sequelize.NULL
}
});
await util.migrationAddBelongsTo(queryInterface, "Stations", "Users", {
name: "lastUpdatedBy"
Expand All @@ -35,7 +47,11 @@ module.exports = {
name: "lastUpdatedBy"
});
await util.migrationRemoveBelongsTo(queryInterface, "Stations", "Groups");
await util.migrationRemoveBelongsTo(queryInterface, "Recordings", "Stations");
await util.migrationRemoveBelongsTo(
queryInterface,
"Recordings",
"Stations"
);
await queryInterface.dropTable("Stations");
}
};
Loading

0 comments on commit a455a3a

Please sign in to comment.