tabletcommand-incident 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.buildkite/pipeline.yml +5 -0
- package/.eslintrc.js +41 -0
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/build/index.js +49 -0
- package/build/index.js.map +1 -0
- package/build/location.js +138 -0
- package/build/location.js.map +1 -0
- package/build/set-incident-type.js +107 -0
- package/build/set-incident-type.js.map +1 -0
- package/build/store.js +116 -0
- package/build/store.js.map +1 -0
- package/build/test/set-incident-type.js +30 -0
- package/build/test/set-incident-type.js.map +1 -0
- package/build/types.js +3 -0
- package/build/types.js.map +1 -0
- package/cspell.json +57 -0
- package/definitions/index.d.ts +8 -0
- package/definitions/index.d.ts.map +1 -0
- package/definitions/location.d.ts +11 -0
- package/definitions/location.d.ts.map +1 -0
- package/definitions/set-incident-type.d.ts +20 -0
- package/definitions/set-incident-type.d.ts.map +1 -0
- package/definitions/store.d.ts +11 -0
- package/definitions/store.d.ts.map +1 -0
- package/definitions/test/set-incident-type.d.ts +2 -0
- package/definitions/test/set-incident-type.d.ts.map +1 -0
- package/definitions/types.d.ts +35 -0
- package/definitions/types.d.ts.map +1 -0
- package/gulpfile.js +19 -0
- package/package.json +54 -0
- package/src/index.ts +60 -0
- package/src/location.ts +167 -0
- package/src/set-incident-type.ts +135 -0
- package/src/store.ts +133 -0
- package/src/test/set-incident-type.ts +31 -0
- package/src/tsconfig.json +30 -0
- package/src/types.ts +39 -0
- package/test.sh +32 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CADIncident } from "tabletcommand-backend-models";
|
|
2
|
+
export interface IncidentTypeConfiguration {
|
|
3
|
+
name: string;
|
|
4
|
+
value: string;
|
|
5
|
+
callTypeDescription: string[];
|
|
6
|
+
callType: string[];
|
|
7
|
+
allowPartialMatch?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare type NotificationType = Pick<IncidentTypeConfiguration, "name" | "value">;
|
|
10
|
+
declare function setIncidentTypeAny(incident: Partial<CADIncident>): void;
|
|
11
|
+
declare function setIncidentTypeUsingConfiguration(incident: Partial<CADIncident>, incidentTypes: IncidentTypeConfiguration[]): boolean;
|
|
12
|
+
declare function setIncidentType(incident: Partial<CADIncident>, incidentTypes: IncidentTypeConfiguration[]): void;
|
|
13
|
+
declare const setIncidentTypeModule: {
|
|
14
|
+
setIncidentTypeAny: typeof setIncidentTypeAny;
|
|
15
|
+
setIncidentTypeUsingConfiguration: typeof setIncidentTypeUsingConfiguration;
|
|
16
|
+
setIncidentType: typeof setIncidentType;
|
|
17
|
+
};
|
|
18
|
+
export default setIncidentTypeModule;
|
|
19
|
+
export declare type SetIncidentTypeModule = typeof setIncidentTypeModule;
|
|
20
|
+
//# sourceMappingURL=set-incident-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set-incident-type.d.ts","sourceRoot":"","sources":["../src/set-incident-type.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAE3D,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,oBAAY,gBAAgB,GAAG,IAAI,CAAC,yBAAyB,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAGjF,iBAAS,kBAAkB,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,CAAC,QAmBzD;AA6BD,iBAAS,iCAAiC,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,aAAa,EAAE,yBAAyB,EAAE,WAmCpH;AAqBD,iBAAS,eAAe,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,aAAa,EAAE,yBAAyB,EAAE,QAOlG;AAED,QAAA,MAAM,qBAAqB;;;;CAI1B,CAAC;AAEF,eAAe,qBAAqB,CAAC;AACrC,oBAAY,qBAAqB,GAAG,OAAO,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="mongoose" />
|
|
2
|
+
import { DepartmentModel, Location, LocationModel, CADVehicleModel, CADVehicle } from "tabletcommand-backend-models";
|
|
3
|
+
import { LocationPayload } from "./types";
|
|
4
|
+
export declare function store(departmentModel: DepartmentModel, locationModel: LocationModel, cadVehicleModel: CADVehicleModel): {
|
|
5
|
+
getVehicle: (item: LocationPayload, departmentId: string) => Promise<import("mongoose").DocumentDefinition<CADVehicle> | null>;
|
|
6
|
+
updateVehicle: (item: Partial<CADVehicle>, departmentId: string) => Promise<CADVehicle | null>;
|
|
7
|
+
updateLocation: (item: Partial<Location>, atDate: Date) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export default store;
|
|
10
|
+
export declare type StoreModule = ReturnType<typeof store>;
|
|
11
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":";AACA,OAAO,EACL,eAAe,EACf,QAAQ,EACR,aAAa,EACb,eAAe,EACf,UAAU,EACX,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAkB,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1D,wBAAgB,KAAK,CACnB,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,eAAe;uBAIA,eAAe,gBAAgB,MAAM;0BAQlC,QAAQ,UAAU,CAAC,gBAAgB,MAAM;2BAaxC,QAAQ,QAAQ,CAAC,UAAU,IAAI;EA0FpE;AACD,eAAe,KAAK,CAAC;AACrB,oBAAY,WAAW,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set-incident-type.d.ts","sourceRoot":"","sources":["../../src/test/set-incident-type.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Location } from "tabletcommand-backend-models";
|
|
2
|
+
export interface LocationPayload {
|
|
3
|
+
latitude: number | string;
|
|
4
|
+
longitude: number | string;
|
|
5
|
+
vehicleId: string;
|
|
6
|
+
radioName: string;
|
|
7
|
+
avlTime: string;
|
|
8
|
+
speed?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface Vehicle {
|
|
11
|
+
departmentId: string;
|
|
12
|
+
modifiedDate: number;
|
|
13
|
+
modified: Date;
|
|
14
|
+
radioName: string;
|
|
15
|
+
vehicleId: string;
|
|
16
|
+
}
|
|
17
|
+
export interface decodedLocation {
|
|
18
|
+
cadAVL: Partial<Location>;
|
|
19
|
+
isValid: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface LocationDelta {
|
|
22
|
+
active?: boolean;
|
|
23
|
+
agencyCode?: string;
|
|
24
|
+
agencyName?: string;
|
|
25
|
+
device_type?: string;
|
|
26
|
+
opAreaCode?: string;
|
|
27
|
+
opAreaName?: string;
|
|
28
|
+
shared?: boolean;
|
|
29
|
+
state?: string;
|
|
30
|
+
username?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare type LocationObject = {
|
|
33
|
+
[P in keyof Location]: Location[P];
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,OAAO;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,oBAAY,cAAc,GAAG;KAC1B,CAAC,IAAI,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC;CACnC,CAAA"}
|
package/gulpfile.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const gulp = require("gulp");
|
|
2
|
+
const shell = require("gulp-shell");
|
|
3
|
+
const del = require("del");
|
|
4
|
+
|
|
5
|
+
gulp.task("clean", function() {
|
|
6
|
+
return del("build/**", {
|
|
7
|
+
force: true
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
gulp.task("ts", gulp.series("clean", shell.task("tsc -p ./src")));
|
|
12
|
+
|
|
13
|
+
gulp.task("lint", gulp.series(shell.task("eslint ./src")));
|
|
14
|
+
|
|
15
|
+
gulp.task("watch", gulp.series("clean", shell.task("tsc -p ./src --watch")));
|
|
16
|
+
|
|
17
|
+
gulp.task("build", gulp.series("ts"));
|
|
18
|
+
|
|
19
|
+
gulp.task("default", gulp.series("build"));
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tabletcommand-incident",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tablet Command Incident",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "gulp build",
|
|
8
|
+
"lint": "eslint src",
|
|
9
|
+
"test": "npx type-coverage && npx mocha -r ts-node/register --reporter list --recursive src/test/**/*.ts"
|
|
10
|
+
},
|
|
11
|
+
"author": "Marius <marius@tabletcommand.com>",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"types": "definitions/index.d.ts",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"debug": "^4.3.4",
|
|
16
|
+
"lodash": "~4.17.21",
|
|
17
|
+
"tabletcommand-backend-models": "~5.35.5"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/chai": "^4.3.11",
|
|
21
|
+
"@types/debug": "^4.1.12",
|
|
22
|
+
"@types/express": "^4.17.21",
|
|
23
|
+
"@types/lodash": "^4.14.202",
|
|
24
|
+
"@types/mocha": "^10.0.6",
|
|
25
|
+
"@types/mongodb": "~3.6.20",
|
|
26
|
+
"@types/mongoose": "~5.10.5",
|
|
27
|
+
"@types/node": "^20.10.6",
|
|
28
|
+
"@types/uuid": "^9.0.7",
|
|
29
|
+
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
|
30
|
+
"@typescript-eslint/parser": "^6.17.0",
|
|
31
|
+
"bson": "~4.7.0",
|
|
32
|
+
"chai": "^4.3.10",
|
|
33
|
+
"cspell": "^7.3.8",
|
|
34
|
+
"del": "^6.0.0",
|
|
35
|
+
"eslint": "^8.56.0",
|
|
36
|
+
"eslint-plugin-node": "^11.1.0",
|
|
37
|
+
"eslint-plugin-promise": "^6.1.1",
|
|
38
|
+
"eslint-plugin-security": "^1.7.1",
|
|
39
|
+
"gulp": "^4.0.2",
|
|
40
|
+
"gulp-shell": "^0.8.0",
|
|
41
|
+
"mocha": "^10.2.0",
|
|
42
|
+
"mongoose": "~5.10.19",
|
|
43
|
+
"ts-node": "^10.9.2",
|
|
44
|
+
"type-coverage": "^2.27.1",
|
|
45
|
+
"typescript": "~4.7.4"
|
|
46
|
+
},
|
|
47
|
+
"typeCoverage": {
|
|
48
|
+
"atLeast": 99.7,
|
|
49
|
+
"ignoreCatch": true,
|
|
50
|
+
"ignoreFiles": [
|
|
51
|
+
"src/test/**/*.ts"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DepartmentModel,
|
|
3
|
+
LocationModel,
|
|
4
|
+
CADVehicleModel,
|
|
5
|
+
Department,
|
|
6
|
+
} from "tabletcommand-backend-models";
|
|
7
|
+
import _ from "lodash";
|
|
8
|
+
|
|
9
|
+
import locationModule from "./location";
|
|
10
|
+
import storeModule from "./store";
|
|
11
|
+
import { LocationPayload } from "./types";
|
|
12
|
+
|
|
13
|
+
export function indexFile(
|
|
14
|
+
departmentModel: DepartmentModel,
|
|
15
|
+
locationModel: LocationModel,
|
|
16
|
+
cadVehicleModel: CADVehicleModel,
|
|
17
|
+
) {
|
|
18
|
+
const store = storeModule(departmentModel, locationModel, cadVehicleModel);
|
|
19
|
+
const location = locationModule(store);
|
|
20
|
+
// async function processVehicle(department: Partial<Department>, item: Partial<CADVehicle>) {
|
|
21
|
+
// // await location.processVehicle()
|
|
22
|
+
// }
|
|
23
|
+
|
|
24
|
+
async function processLocations(department: Partial<Department>, items: LocationPayload[] | null | undefined) {
|
|
25
|
+
const atDate = new Date();
|
|
26
|
+
|
|
27
|
+
if (_.isUndefined(items) || _.isNull(items)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
if (_.isObject(items) && !_.isArray(items)) {
|
|
31
|
+
items = [items];
|
|
32
|
+
}
|
|
33
|
+
const result = await location.processItems(department, items, atDate);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// async function processLocationsOnly(department: Partial<Department>, items: Partial<Location>[] | null | undefined, agency: Partial<Agency>) {
|
|
38
|
+
// try {
|
|
39
|
+
// if (_.isUndefined(items) || _.isNull(items)) {
|
|
40
|
+
// return [];
|
|
41
|
+
// }
|
|
42
|
+
// if (_.isObject(items) && !_.isArray(items)) {
|
|
43
|
+
// items = [items];
|
|
44
|
+
// }
|
|
45
|
+
// const result = await location.processItemsNoVehicle(department, items, agency);
|
|
46
|
+
// return result;
|
|
47
|
+
// } catch (err) {
|
|
48
|
+
// throw err;
|
|
49
|
+
// }
|
|
50
|
+
// }
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
// processVehicle,
|
|
54
|
+
processLocations,
|
|
55
|
+
// processLocationsOnly,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default indexFile;
|
|
60
|
+
export type IncidentModule = ReturnType<typeof indexFile>;
|
package/src/location.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import debugModule from "debug";
|
|
3
|
+
import mongoose from "mongoose";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Department,
|
|
7
|
+
Location,
|
|
8
|
+
CADVehicle,
|
|
9
|
+
} from "tabletcommand-backend-models";
|
|
10
|
+
import {
|
|
11
|
+
LocationPayload, decodedLocation,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { StoreModule } from "./store";
|
|
14
|
+
|
|
15
|
+
export function location(store: StoreModule) {
|
|
16
|
+
const debug = debugModule("tabletcommand-location:location");
|
|
17
|
+
|
|
18
|
+
async function decodeCADVehicleStatusToLocation(item: LocationPayload, department: Partial<Department>, atDate: Date): Promise<decodedLocation> {
|
|
19
|
+
let latitude = 0;
|
|
20
|
+
let longitude = 0;
|
|
21
|
+
|
|
22
|
+
if (_.isFinite(parseFloat(item.latitude.toString()))) {
|
|
23
|
+
latitude = parseFloat(item.latitude.toString());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (_.isFinite(parseFloat(item.longitude.toString()))) {
|
|
27
|
+
longitude = parseFloat(item.longitude.toString());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!_.isString(item.radioName)) {
|
|
31
|
+
item.radioName = "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!_.isString(item.vehicleId)) {
|
|
35
|
+
item.vehicleId = "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const now = atDate.valueOf() / 1000.0;
|
|
39
|
+
// Don't save provided AVL time if it's more than 30s in the future
|
|
40
|
+
const futureTimeLimit = now + 30;
|
|
41
|
+
// If present, replace the modified unix date with the one provided by the CAD
|
|
42
|
+
let movedAtUnix = now;
|
|
43
|
+
if (_.isString(item.avlTime) && _.trim(item.avlTime) !== "") {
|
|
44
|
+
const checkISODate = (new Date(item.avlTime).valueOf()) / 1000.0;
|
|
45
|
+
const cadUnixTime = _.isNaN(checkISODate) && _.isString(item.avlTime) ? parseFloat(item.avlTime) : checkISODate;
|
|
46
|
+
|
|
47
|
+
if (_.isFinite(cadUnixTime) && cadUnixTime <= futureTimeLimit) {
|
|
48
|
+
movedAtUnix = cadUnixTime;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const userId = `${item.radioName}-${item.vehicleId}`; // Fake user id radioName - vehicleId
|
|
53
|
+
|
|
54
|
+
let sharingEnabled = false;
|
|
55
|
+
let opAreaName = "";
|
|
56
|
+
let opAreaCode = "";
|
|
57
|
+
let deptState = "";
|
|
58
|
+
let locationStaleMinutes = 31 * 24 * 60; // 31d
|
|
59
|
+
|
|
60
|
+
if (_.isObject(department) && _.isObject(department.shareAVL)) {
|
|
61
|
+
sharingEnabled = department.shareAVL.enabled && department.accountType === "production";
|
|
62
|
+
opAreaName = department.shareAVL.opAreaName || "";
|
|
63
|
+
opAreaCode = department.shareAVL.opAreaCode || "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (_.isObject(department)) {
|
|
67
|
+
if (_.isObject(department.addressDetails) && _.isString(department.addressDetails.state)) {
|
|
68
|
+
deptState = department.addressDetails.state || "";
|
|
69
|
+
}
|
|
70
|
+
if (_.isNumber(department.locationStaleMinutes) && _.isFinite(department.locationStaleMinutes) && department.locationStaleMinutes > 0) {
|
|
71
|
+
locationStaleMinutes = department.locationStaleMinutes;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const deleteAfterDate = new Date(atDate.valueOf() + 1000 * 60 * locationStaleMinutes).toISOString();
|
|
75
|
+
|
|
76
|
+
const cadAVL: Partial<Location> = {
|
|
77
|
+
device_type: "cad",
|
|
78
|
+
locationGeoJSON: {
|
|
79
|
+
type: "Point",
|
|
80
|
+
coordinates: [longitude, latitude]
|
|
81
|
+
},
|
|
82
|
+
deleteAfterDate,
|
|
83
|
+
modified: atDate.toISOString(),
|
|
84
|
+
movedAt: new Date(movedAtUnix * 1000).toISOString(),
|
|
85
|
+
active: true,
|
|
86
|
+
username: item.radioName,
|
|
87
|
+
userId,
|
|
88
|
+
departmentId: (department._id as mongoose.Types.ObjectId).toString(),
|
|
89
|
+
session: userId,
|
|
90
|
+
version: 2,
|
|
91
|
+
shared: sharingEnabled,
|
|
92
|
+
opAreaName,
|
|
93
|
+
opAreaCode,
|
|
94
|
+
state: deptState,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (_.isNumber(item.speed)) {
|
|
98
|
+
const speed = item.speed;
|
|
99
|
+
cadAVL.speed = speed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hasCoordinate = Math.abs(latitude) > 0.01 || Math.abs(longitude) > 0.01;
|
|
103
|
+
const validCoordinate = Math.abs(latitude) < 90 && Math.abs(longitude) < 180;
|
|
104
|
+
const hasVehicle = _.isString(item.vehicleId) && _.trim(item.vehicleId).length > 0;
|
|
105
|
+
const hasRadioName = _.isString(item.radioName) && _.trim(item.radioName).length > 0;
|
|
106
|
+
const isValid = hasCoordinate && validCoordinate && (hasVehicle || hasRadioName);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
cadAVL,
|
|
110
|
+
isValid,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function processVehicle(departmentExceptions: string[], item: LocationPayload, department: Partial<Department>, atDate: Date) {
|
|
115
|
+
const departmentId: string = (department._id as mongoose.Types.ObjectId).toString();
|
|
116
|
+
|
|
117
|
+
// need function to validate the item (As concerns vehicleId and radioName)
|
|
118
|
+
const isBidirectionalException = departmentExceptions.indexOf(departmentId) !== -1;
|
|
119
|
+
// Determine if vehicle is active or not
|
|
120
|
+
const cadItem = await store.getVehicle(item, departmentId);
|
|
121
|
+
if (!_.isObject(cadItem) &&
|
|
122
|
+
department.cadOneWayVehiclesEnabled &&
|
|
123
|
+
(department.cadBidirectionalEnabled !== true || isBidirectionalException)) {
|
|
124
|
+
const vehicleItem: Partial<CADVehicle> = {
|
|
125
|
+
departmentId: departmentId,
|
|
126
|
+
modifiedDate: atDate.valueOf() / 1000.0,
|
|
127
|
+
modified: atDate.toISOString(),
|
|
128
|
+
radioName: item.radioName,
|
|
129
|
+
vehicleId: item.vehicleId,
|
|
130
|
+
};
|
|
131
|
+
await store.updateVehicle(vehicleItem, departmentId);
|
|
132
|
+
}
|
|
133
|
+
return cadItem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function processItems(department: Partial<Department>, items: LocationPayload[], atDate: Date) {
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
const decoded = await decodeCADVehicleStatusToLocation(item, department, atDate);
|
|
139
|
+
debug(`processCADVehicleLocation item: ${JSON.stringify(item)} d:${JSON.stringify(department)} at: ${atDate.toISOString()}.`);
|
|
140
|
+
if (!decoded.isValid) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let active = true;
|
|
145
|
+
const cadBidirectionalExceptions = [
|
|
146
|
+
"627d718b8d6145748b30282b" // HMT (2 way comments, 1 way vehicles)
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const cadItem = await processVehicle(cadBidirectionalExceptions, item, department, atDate);
|
|
150
|
+
|
|
151
|
+
if (_.isObject(cadItem) && _.isBoolean(cadItem.mapHidden)) {
|
|
152
|
+
active = !cadItem.mapHidden;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
decoded.cadAVL.active = active;
|
|
156
|
+
await store.updateLocation(decoded.cadAVL, atDate);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
processVehicle,
|
|
162
|
+
processItems,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default location;
|
|
167
|
+
export type LocationModule = ReturnType<typeof location>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import { CADIncident } from "tabletcommand-backend-models";
|
|
3
|
+
|
|
4
|
+
export interface IncidentTypeConfiguration {
|
|
5
|
+
name: string,
|
|
6
|
+
value: string,
|
|
7
|
+
callTypeDescription: string[],
|
|
8
|
+
callType: string[],
|
|
9
|
+
allowPartialMatch?: boolean,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type NotificationType = Pick<IncidentTypeConfiguration, "name" | "value">;
|
|
13
|
+
|
|
14
|
+
// Change incident by reference
|
|
15
|
+
function setIncidentTypeAny(incident: Partial<CADIncident>) {
|
|
16
|
+
if (!_.isArray(incident.notificationType)) {
|
|
17
|
+
incident.notificationType = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let foundTypeAny = false;
|
|
21
|
+
_.map(incident.notificationType, function(t) {
|
|
22
|
+
if (_.isString(t.value) && t.value === "any") {
|
|
23
|
+
foundTypeAny = true;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!foundTypeAny) {
|
|
29
|
+
incident.notificationType.push({
|
|
30
|
+
name: "Any",
|
|
31
|
+
value: "any"
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function matchCurrentAgainstTypes(value: string, types: string[] | undefined, allowPartialMatch: boolean | undefined, resolveTo: NotificationType): NotificationType | null {
|
|
37
|
+
let found: NotificationType | null = null;
|
|
38
|
+
|
|
39
|
+
if (value === "") {
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!_.isArray(types)) {
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
types.forEach((callTypeItem) => {
|
|
48
|
+
// If incident contains that call type, consider it a match
|
|
49
|
+
// This will address the situations when callTypeDescription has a
|
|
50
|
+
// dynamic prefix not defined in the call type array
|
|
51
|
+
const lowerCasedCallTypeItem = callTypeItem.toLowerCase();
|
|
52
|
+
const containsMatch = value.includes(lowerCasedCallTypeItem);
|
|
53
|
+
const exactMatch = value === lowerCasedCallTypeItem;
|
|
54
|
+
const isMatch = exactMatch || (containsMatch && allowPartialMatch === true);
|
|
55
|
+
if (isMatch) {
|
|
56
|
+
found = resolveTo;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function setIncidentTypeUsingConfiguration(incident: Partial<CADIncident>, incidentTypes: IncidentTypeConfiguration[]) {
|
|
64
|
+
let matched = false;
|
|
65
|
+
if (!_.isArray(incidentTypes) || _.size(incidentTypes) === 0) {
|
|
66
|
+
return matched;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let incidentCallTypeDescription = "";
|
|
70
|
+
if (_.isString(incident.AgencyIncidentCallTypeDescription) && incident.AgencyIncidentCallTypeDescription !== "") {
|
|
71
|
+
incidentCallTypeDescription = incident.AgencyIncidentCallTypeDescription.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let incidentCallType = "";
|
|
75
|
+
if (_.isString(incident.AgencyIncidentCallType) && incident.AgencyIncidentCallType !== "") {
|
|
76
|
+
incidentCallType = incident.AgencyIncidentCallType.toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_.each(incidentTypes, function(item) {
|
|
80
|
+
if (incidentCallType !== "") {
|
|
81
|
+
const foundType = matchCurrentAgainstTypes(incidentCallType, item.callType, item.allowPartialMatch, {
|
|
82
|
+
name: item.name,
|
|
83
|
+
value: item.value,
|
|
84
|
+
});
|
|
85
|
+
matched = appendNotification(incident, foundType);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (incidentCallTypeDescription !== "") {
|
|
89
|
+
const foundTypeDescription = matchCurrentAgainstTypes(incidentCallTypeDescription, item.callTypeDescription, item.allowPartialMatch, {
|
|
90
|
+
name: item.name,
|
|
91
|
+
value: item.value,
|
|
92
|
+
});
|
|
93
|
+
matched = appendNotification(incident, foundTypeDescription);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return matched;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function appendNotification(incident: Partial<CADIncident>, foundType: NotificationType | null): boolean {
|
|
101
|
+
if (foundType === null) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!_.isArray(incident.notificationType)) {
|
|
106
|
+
incident.notificationType = [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const existingMatches = incident.notificationType.filter((n) => n.value === foundType.value);
|
|
110
|
+
// This item was already added
|
|
111
|
+
if (existingMatches.length > 0) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
incident.notificationType.push(foundType);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setIncidentType(incident: Partial<CADIncident>, incidentTypes: IncidentTypeConfiguration[]) {
|
|
120
|
+
setIncidentTypeAny(incident);
|
|
121
|
+
|
|
122
|
+
const hasDatabaseConfiguration = _.isArray(incidentTypes) && incidentTypes.length > 0;
|
|
123
|
+
if (hasDatabaseConfiguration) {
|
|
124
|
+
setIncidentTypeUsingConfiguration(incident, incidentTypes);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const setIncidentTypeModule = {
|
|
129
|
+
setIncidentTypeAny,
|
|
130
|
+
setIncidentTypeUsingConfiguration,
|
|
131
|
+
setIncidentType
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default setIncidentTypeModule;
|
|
135
|
+
export type SetIncidentTypeModule = typeof setIncidentTypeModule;
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import debugModule from "debug";
|
|
2
|
+
import {
|
|
3
|
+
DepartmentModel,
|
|
4
|
+
Location,
|
|
5
|
+
LocationModel,
|
|
6
|
+
CADVehicleModel,
|
|
7
|
+
CADVehicle,
|
|
8
|
+
} from "tabletcommand-backend-models";
|
|
9
|
+
import _ from "lodash";
|
|
10
|
+
|
|
11
|
+
import { LocationObject, LocationPayload } from "./types";
|
|
12
|
+
|
|
13
|
+
export function store(
|
|
14
|
+
departmentModel: DepartmentModel,
|
|
15
|
+
locationModel: LocationModel,
|
|
16
|
+
cadVehicleModel: CADVehicleModel,
|
|
17
|
+
) {
|
|
18
|
+
const debug = debugModule("tabletcommand-location:location:store");
|
|
19
|
+
|
|
20
|
+
async function getVehicle(item: LocationPayload, departmentId: string) {
|
|
21
|
+
const query = {
|
|
22
|
+
vehicleId: item.vehicleId,
|
|
23
|
+
departmentId,
|
|
24
|
+
};
|
|
25
|
+
return cadVehicleModel.findOne(query).lean();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function updateVehicle(item: Partial<CADVehicle>, departmentId: string) {
|
|
29
|
+
const query = {
|
|
30
|
+
departmentId,
|
|
31
|
+
vehicleId: item.vehicleId,
|
|
32
|
+
};
|
|
33
|
+
const options = {
|
|
34
|
+
upsert: true,
|
|
35
|
+
setDefaultsOnInsert: true,
|
|
36
|
+
new: true
|
|
37
|
+
};
|
|
38
|
+
return await cadVehicleModel.findOneAndUpdate(query, item, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function updateLocation(item: Partial<Location>, atDate: Date) {
|
|
42
|
+
const modelItem = new locationModel(item);
|
|
43
|
+
const query = {
|
|
44
|
+
device_type: modelItem.device_type,
|
|
45
|
+
departmentId: modelItem.departmentId,
|
|
46
|
+
username: modelItem.username
|
|
47
|
+
};
|
|
48
|
+
debug(`Location.findOne: ${JSON.stringify(query)}`);
|
|
49
|
+
const dbItem = await locationModel.findOne(query).lean().exec() as Partial<LocationObject>;
|
|
50
|
+
const cloneItem = { ...dbItem };
|
|
51
|
+
let updatedItem = propagateLocation(modelItem, dbItem);
|
|
52
|
+
debug(`Location.save: ${JSON.stringify(updatedItem)}`);
|
|
53
|
+
if (cloneItem._id) {
|
|
54
|
+
const locationDelta = generateLocationDelta(cloneItem, updatedItem, atDate);
|
|
55
|
+
updatedItem = locationDelta;
|
|
56
|
+
debug(`Location.delta: ${JSON.stringify(locationDelta)}`);
|
|
57
|
+
} else {
|
|
58
|
+
// if new location record - Attempt to reconcile agency name and code to resources
|
|
59
|
+
// this is going to be resolved by the bin/cron-cleanup-minute cron
|
|
60
|
+
}
|
|
61
|
+
await locationModel.updateOne(query, {
|
|
62
|
+
$set: updatedItem,
|
|
63
|
+
}, {
|
|
64
|
+
upsert: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function propagateLocation(modelItem: Location, dbItem: Partial<LocationObject>) {
|
|
69
|
+
if (!_.isObject(dbItem)) {
|
|
70
|
+
return modelItem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// We keep the same value for _id, uuid, departmentId
|
|
74
|
+
dbItem.active = modelItem.active;
|
|
75
|
+
dbItem.altitude = modelItem.altitude;
|
|
76
|
+
dbItem.device_type = modelItem.device_type;
|
|
77
|
+
dbItem.heading = modelItem.heading;
|
|
78
|
+
dbItem.modified = modelItem.modified;
|
|
79
|
+
dbItem.movedAt = modelItem.movedAt;
|
|
80
|
+
dbItem.session = modelItem.session;
|
|
81
|
+
dbItem.speed = modelItem.speed;
|
|
82
|
+
dbItem.userId = modelItem.userId;
|
|
83
|
+
dbItem.username = modelItem.username;
|
|
84
|
+
dbItem.version = modelItem.version;
|
|
85
|
+
dbItem.locationGeoJSON = modelItem.locationGeoJSON;
|
|
86
|
+
|
|
87
|
+
// Propagate sharedAVL items
|
|
88
|
+
dbItem.opAreaCode = modelItem.opAreaCode;
|
|
89
|
+
dbItem.opAreaName = modelItem.opAreaName;
|
|
90
|
+
dbItem.shared = modelItem.shared;
|
|
91
|
+
dbItem.state = modelItem.state;
|
|
92
|
+
|
|
93
|
+
return dbItem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function generateLocationDelta(locationRecord: Partial<LocationObject>, updateObject: Partial<LocationObject>, atDate: Date) {
|
|
97
|
+
if (!_.isObject(locationRecord)) {
|
|
98
|
+
return updateObject;
|
|
99
|
+
}
|
|
100
|
+
const locationDelta: Partial<LocationObject> = {};
|
|
101
|
+
for (const [key, value] of Object.entries(updateObject)) {
|
|
102
|
+
if (JSON.stringify(value) !== JSON.stringify(locationRecord[key])) {
|
|
103
|
+
locationDelta[key] = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const updatedKeys = Object.keys(locationDelta);
|
|
108
|
+
const propsKeys = [
|
|
109
|
+
"active",
|
|
110
|
+
"agencyCode",
|
|
111
|
+
"agencyName",
|
|
112
|
+
"device_type",
|
|
113
|
+
"opAreaCode",
|
|
114
|
+
"opAreaName",
|
|
115
|
+
"shared",
|
|
116
|
+
"state",
|
|
117
|
+
"username",
|
|
118
|
+
];
|
|
119
|
+
if (_.intersection(updatedKeys, propsKeys).length > 0) {
|
|
120
|
+
locationDelta.propsChangedAt = atDate.toISOString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return locationDelta;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
getVehicle,
|
|
128
|
+
updateVehicle,
|
|
129
|
+
updateLocation,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export default store;
|
|
133
|
+
export type StoreModule = ReturnType<typeof store>;
|