tabletcommand-location 1.0.1

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/cspell.json ADDED
@@ -0,0 +1,57 @@
1
+ // cSpell Settings
2
+ {
3
+ "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
4
+ "version": "0.2",
5
+ // language - current active spelling language
6
+ "language": "en-US",
7
+ // words - list of words to be always considered correct
8
+ "words": [
9
+ "backend",
10
+ "bamse",
11
+ "devops",
12
+ "dmon",
13
+ "helloworld",
14
+ "mget",
15
+ "mockgoose",
16
+ "signup",
17
+ "stacktraces",
18
+ "tabletcommand",
19
+ "tyrion",
20
+ "unixtime",
21
+ "Ursache",
22
+ "personnelapi",
23
+ "agencypersonnelapi",
24
+ "expireat",
25
+ "agencyapi",
26
+ "accesslog",
27
+ "hincrby",
28
+ "hgetall",
29
+ "myhash",
30
+ "virtuals",
31
+ "apikey",
32
+ "personnelapikey",
33
+ "signupkey",
34
+ "fdid",
35
+ "flushall",
36
+ "zonehaven"
37
+ ],
38
+ // flagWords - list of words to be always considered incorrect
39
+ // This is useful for offensive words and common spelling errors.
40
+ // For example "hte" should be "the"
41
+ "flagWords": [
42
+ "hte"
43
+ ],
44
+ "ignoreWords": [],
45
+ "languageSettings": [{
46
+ "languageId": "*",
47
+ "dictionaries": []
48
+ }],
49
+ "ignorePaths": [
50
+ "node_modules",
51
+ "src/**/*.svg",
52
+ "/project-words.txt"
53
+ ],
54
+ "files": [
55
+ "src/**"
56
+ ]
57
+ }
@@ -0,0 +1,8 @@
1
+ import { DepartmentModel, LocationModel, CADVehicleModel, Department } from "tabletcommand-backend-models";
2
+ import { LocationPayload } from "./types";
3
+ export declare function lib(departmentModel: DepartmentModel, locationModel: LocationModel, cadVehicleModel: CADVehicleModel): {
4
+ processLocations: (department: Partial<Department>, items: LocationPayload[] | null | undefined) => Promise<void | never[]>;
5
+ };
6
+ export default lib;
7
+ export declare type LibModule = ReturnType<typeof lib>;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,eAAe,EACf,aAAa,EACb,eAAe,EAGf,UAAU,EAEX,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,wBAAgB,GAAG,CACjB,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,eAAe;mCAQY,QAAQ,UAAU,CAAC,SAAS,eAAe,EAAE,GAAG,IAAI,GAAG,SAAS;EAoC7G;AAED,eAAe,GAAG,CAAC;AACnB,oBAAY,SAAS,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { Department, CADVehicle } from "tabletcommand-backend-models";
2
+ import { LocationPayload } from "./types";
3
+ import { StoreModule } from "./store";
4
+ export declare function location(store: StoreModule): {
5
+ processVehicle: (departmentExceptions: string[], item: LocationPayload, department: Partial<Department>, atDate: Date) => Promise<import("mongoose").DocumentDefinition<CADVehicle> | null>;
6
+ processItems: (department: Partial<Department>, items: LocationPayload[], atDate: Date) => Promise<void>;
7
+ };
8
+ export default location;
9
+ export declare type LocationModule = ReturnType<typeof location>;
10
+ //# sourceMappingURL=location.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"location.d.ts","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,UAAU,EAEV,UAAU,EACX,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW;2CA8FW,MAAM,EAAE,QAAQ,eAAe,cAAc,QAAQ,UAAU,CAAC,UAAU,IAAI;+BAoB1F,QAAQ,UAAU,CAAC,SAAS,eAAe,EAAE,UAAU,IAAI;EA4BpG;AAED,eAAe,QAAQ,CAAC;AACxB,oBAAY,cAAc,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { DepartmentModel, Location, LocationModel, CADVehicleModel, CADVehicle } from "tabletcommand-backend-models";
2
+ import { LocationPayload } from "./types";
3
+ export declare function store(departmentModel: DepartmentModel, locationModel: LocationModel, cadVehicleModel: CADVehicleModel): {
4
+ getVehicle: (item: LocationPayload, departmentId: string) => Promise<import("mongoose").DocumentDefinition<CADVehicle> | null>;
5
+ updateVehicle: (item: Partial<CADVehicle>, departmentId: string) => Promise<CADVehicle | null>;
6
+ updateLocation: (item: Partial<Location>, atDate: Date) => Promise<void>;
7
+ };
8
+ export default store;
9
+ export declare type StoreModule = ReturnType<typeof store>;
10
+ //# 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,EAEf,QAAQ,EACR,aAAa,EACb,eAAe,EACf,UAAU,EACX,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAiC,eAAe,EAAW,MAAM,SAAS,CAAC;AAElF,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,2 @@
1
+ import "mocha";
2
+ //# sourceMappingURL=location.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"location.d.ts","sourceRoot":"","sources":["../../src/test/location.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,CAAC"}
@@ -0,0 +1,63 @@
1
+ import * as mongoose from "mongoose";
2
+ import "mocha";
3
+ import { BackendModels, Agency, Department } from "tabletcommand-backend-models";
4
+ export default function (modelsModule: typeof import("tabletcommand-backend-models")): {
5
+ department: Partial<Department>;
6
+ agency: Partial<Agency>;
7
+ clearTestData: (mods: BackendModels) => Promise<void>;
8
+ prepareTestData: (mods: BackendModels) => Promise<void>;
9
+ before: () => Promise<{
10
+ ActionLog: import("tabletcommand-backend-models").ActionLogModel;
11
+ Agency: import("tabletcommand-backend-models").AgencyModel;
12
+ ArcGISGroup: import("tabletcommand-backend-models").ArcGISGroupModel;
13
+ Assignment: import("tabletcommand-backend-models").AssignmentModel;
14
+ Battalion: import("tabletcommand-backend-models").BattalionModel;
15
+ BeaconLog: import("tabletcommand-backend-models").BeaconLogModel;
16
+ CADIncident: import("tabletcommand-backend-models").CADIncidentModel;
17
+ CADIncidentBlock: import("tabletcommand-backend-models").CADIncidentBlockModel;
18
+ CADIncidentDelta: import("tabletcommand-backend-models").CADIncidentDeltaModel;
19
+ CADIncidentFlat: import("tabletcommand-backend-models").CADIncidentFlatModel;
20
+ CADIncidentStream: import("tabletcommand-backend-models").CADIncidentStreamModel;
21
+ CADStatus: import("tabletcommand-backend-models").CADStatusModel;
22
+ CADStatusMap: import("tabletcommand-backend-models").CADStatusMapModel;
23
+ CADVehicle: import("tabletcommand-backend-models").CADVehicleModel;
24
+ CADVehicleStatus: import("tabletcommand-backend-models").CADVehicleStatusModel;
25
+ CADVehicleStatusHistory: import("tabletcommand-backend-models").CADVehicleStatusHistoryModel;
26
+ ChartDeviceStats: import("tabletcommand-backend-models").ChartDeviceStatsModel;
27
+ ChartIncident: import("tabletcommand-backend-models").ChartIncidentModel;
28
+ ChartManagedIncident: import("tabletcommand-backend-models").ChartManagedIncidentModel;
29
+ ChartUser: import("tabletcommand-backend-models").ChartUserModel;
30
+ Checklist: import("tabletcommand-backend-models").ChecklistModel;
31
+ ChecklistItem: import("tabletcommand-backend-models").ChecklistItemModel;
32
+ CSVImport: import("tabletcommand-backend-models").CSVImportModel;
33
+ Department: import("tabletcommand-backend-models").DepartmentModel;
34
+ DeviceMapping: import("tabletcommand-backend-models").DeviceMappingModel;
35
+ Esri: import("tabletcommand-backend-models").EsriModel;
36
+ GSTMapping: import("tabletcommand-backend-models").GSTMappingModel;
37
+ IncidentEvent: import("tabletcommand-backend-models").IncidentEventModel;
38
+ IncidentNotified: import("tabletcommand-backend-models").IncidentNotifiedModel;
39
+ IncidentTakeover: import("tabletcommand-backend-models").IncidentTakeoverModel;
40
+ JobLog: import("tabletcommand-backend-models").JobLogModel;
41
+ Location: import("tabletcommand-backend-models").LocationModel;
42
+ MailLog: import("tabletcommand-backend-models").MailLogModel;
43
+ ManagedIncident: import("tabletcommand-backend-models").ManagedIncidentModel;
44
+ Message: import("tabletcommand-backend-models").MessageModel;
45
+ Monitor: import("tabletcommand-backend-models").MonitorModel;
46
+ PersonnelImport: import("tabletcommand-backend-models").PersonnelImportModel;
47
+ RateLimit: import("tabletcommand-backend-models").RateLimitModel;
48
+ RemoteLog: import("tabletcommand-backend-models").RemoteLogModel;
49
+ Session: import("tabletcommand-backend-models").SessionModel;
50
+ SMTPUnhandled: import("tabletcommand-backend-models").SMTPUnhandledModel;
51
+ Template: import("tabletcommand-backend-models").TemplateModel;
52
+ User: import("tabletcommand-backend-models").UserModel;
53
+ UserDevice: import("tabletcommand-backend-models").UserDeviceModel;
54
+ UserRegistration: import("tabletcommand-backend-models").UserRegistrationModel;
55
+ ValidationReport: import("tabletcommand-backend-models").ValidationReportModel;
56
+ }>;
57
+ beforeEach: () => Promise<void>;
58
+ after: () => Promise<void>;
59
+ afterEach: () => Promise<void>;
60
+ getModels: () => BackendModels;
61
+ getConnection: () => typeof mongoose;
62
+ };
63
+ //# sourceMappingURL=mock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../../src/test/mock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AACrC,OAAO,OAAO,CAAC;AAEf,OAAO,EACL,aAAa,EACb,MAAM,EACN,UAAU,EACX,MAAM,8BAA8B,CAAC;AAEtC,MAAM,CAAC,OAAO,WAAW,YAAY,EAAE,cAAc,8BAA8B,CAAC;;;0BAkE/C,aAAa;4BAQX,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAoD5B,aAAa;yBAIT,eAAe;EAgB1C"}
@@ -0,0 +1,33 @@
1
+ import { Location } from "tabletcommand-backend-models";
2
+ export interface LocationPayload extends Location {
3
+ latitude: number | string;
4
+ longitude: number | string;
5
+ vehicleId: string;
6
+ radioName: string;
7
+ }
8
+ export interface Vehicle {
9
+ departmentId: string;
10
+ modifiedDate: number;
11
+ modified: Date;
12
+ radioName: string;
13
+ vehicleId: string;
14
+ }
15
+ export interface decodedLocation {
16
+ cadAVL: Partial<Location>;
17
+ isValid: boolean;
18
+ }
19
+ export interface LocationDelta {
20
+ active?: boolean;
21
+ agencyCode?: string;
22
+ agencyName?: string;
23
+ device_type?: string;
24
+ opAreaCode?: string;
25
+ opAreaName?: string;
26
+ shared?: boolean;
27
+ state?: string;
28
+ username?: string;
29
+ }
30
+ export declare type LocationObject = {
31
+ [P in keyof Location]: Location[P];
32
+ };
33
+ //# 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,eAAgB,SAAQ,QAAQ;IAC7C,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,OAAO;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC1B,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;CACrB;AAED,oBAAY,cAAc,GAAG;KACxB,CAAC,IAAI,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC;CACrC,CAAC"}
package/gulpfile.js ADDED
@@ -0,0 +1,20 @@
1
+ const gulp = require("gulp");
2
+ const shell = require("gulp-shell");
3
+ const mocha = require("gulp-mocha");
4
+ const del = require("del");
5
+
6
+ gulp.task("clean", function() {
7
+ return del("build/**", {
8
+ force: true
9
+ });
10
+ });
11
+
12
+ gulp.task("ts", gulp.series("clean", shell.task("tsc -p ./src")));
13
+
14
+ gulp.task("lint", gulp.series(shell.task("eslint ./src")));
15
+
16
+ gulp.task("watch", gulp.series("clean", shell.task("tsc -p ./src --watch")));
17
+
18
+ gulp.task("build", gulp.series("ts"));
19
+
20
+ gulp.task("default", gulp.series("build"));
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "tabletcommand-location",
3
+ "version": "1.0.1",
4
+ "description": "Tablet Command Location",
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": "Joe Goeman <joe@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.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/chai": "^4.3.10",
21
+ "@types/debug": "^4.1.12",
22
+ "@types/express": "^4.17.21",
23
+ "@types/lodash": "^4.14.201",
24
+ "@types/mocha": "^10.0.4",
25
+ "@types/mongodb": "~3.6.20",
26
+ "@types/mongoose": "~5.10.5",
27
+ "@types/node": "^20.9.2",
28
+ "@types/uuid": "^9.0.7",
29
+ "@typescript-eslint/eslint-plugin": "^6.11.0",
30
+ "@typescript-eslint/parser": "^6.11.0",
31
+ "bson": "~4.7.0",
32
+ "chai": "^4.3.10",
33
+ "cspell": "^7.3.8",
34
+ "del": "^6.0.0",
35
+ "eslint": "^8.54.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-mocha": "^8.0.0",
41
+ "gulp-shell": "^0.8.0",
42
+ "mocha": "^10.2.0",
43
+ "mongoose": "~5.10.19",
44
+ "ts-node": "^10.9.1",
45
+ "type-coverage": "^2.27.0",
46
+ "typescript": "~4.7.4"
47
+ },
48
+ "typeCoverage": {
49
+ "atLeast": 99.9,
50
+ "ignoreCatch": true,
51
+ "ignoreFiles": [
52
+ "src/test/**/*.ts"
53
+ ],
54
+ "suppressError": true
55
+ }
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,65 @@
1
+ import locationModule from "./location";
2
+ import storeModule from "./store";
3
+ import {
4
+ AgencyModel,
5
+ DepartmentModel,
6
+ LocationModel,
7
+ CADVehicleModel,
8
+ CADVehicle,
9
+ Location,
10
+ Department,
11
+ Agency
12
+ } from "tabletcommand-backend-models";
13
+ import _ from "lodash";
14
+ import { LocationPayload } from "./types";
15
+ export function lib(
16
+ departmentModel: DepartmentModel,
17
+ locationModel: LocationModel,
18
+ cadVehicleModel: CADVehicleModel,
19
+ ) {
20
+ const store = storeModule(departmentModel, locationModel, cadVehicleModel);
21
+ const location = locationModule(store);
22
+ // async function processVehicle(department: Partial<Department>, item: Partial<CADVehicle>) {
23
+ // // await location.processVehicle()
24
+ // }
25
+
26
+ async function processLocations(department: Partial<Department>, items: LocationPayload[] | null | undefined) {
27
+ const atDate = new Date();
28
+ try {
29
+ if (_.isUndefined(items) || _.isNull(items)) {
30
+ return [];
31
+ }
32
+ if (_.isObject(items) && !_.isArray(items)) {
33
+ items = [items];
34
+ }
35
+ const result = await location.processItems(department, items, atDate);
36
+ return result;
37
+ } catch (err) {
38
+ throw err;
39
+ }
40
+ }
41
+
42
+ // async function processLocationsOnly(department: Partial<Department>, items: Partial<Location>[] | null | undefined, agency: Partial<Agency>) {
43
+ // try {
44
+ // if (_.isUndefined(items) || _.isNull(items)) {
45
+ // return [];
46
+ // }
47
+ // if (_.isObject(items) && !_.isArray(items)) {
48
+ // items = [items];
49
+ // }
50
+ // const result = await location.processItemsNoVehicle(department, items, agency);
51
+ // return result;
52
+ // } catch (err) {
53
+ // throw err;
54
+ // }
55
+ // }
56
+
57
+ return {
58
+ // processVehicle,
59
+ processLocations,
60
+ // processLocationsOnly,
61
+ };
62
+ }
63
+
64
+ export default lib;
65
+ export type LibModule = ReturnType<typeof lib>;
@@ -0,0 +1,161 @@
1
+ import _ from "lodash";
2
+ import {
3
+ DepartmentModel,
4
+ LocationModel,
5
+ CADVehicleModel,
6
+ Department,
7
+ Location,
8
+ CADVehicle,
9
+ } from "tabletcommand-backend-models";
10
+ import {
11
+ LocationPayload, Vehicle, decodedLocation,
12
+ } from "./types";
13
+ import debugModule from "debug";
14
+ import { StoreModule } from "./store";
15
+
16
+ export function location(store: StoreModule ) {
17
+ const debug = debugModule("tabletcommand-location:location");
18
+
19
+ async function decodeCADVehicleStatusToLocation(item: LocationPayload, department: Partial<Department>, atDate: Date): Promise<decodedLocation> {
20
+ let latitude = 0;
21
+ let longitude = 0;
22
+
23
+ if (_.isString(item.latitude) && _.isFinite(parseFloat(item.latitude))) {
24
+ latitude = parseFloat(item.latitude);
25
+ }
26
+
27
+ if (_.isString(item.longitude) && _.isFinite(parseFloat(item.longitude))) {
28
+ longitude = parseFloat(item.longitude);
29
+ }
30
+
31
+ if (!_.isString(item.radioName)) {
32
+ item.radioName = "";
33
+ }
34
+
35
+ if (!_.isString(item.vehicleId)) {
36
+ item.vehicleId = "";
37
+ }
38
+
39
+ const now = atDate.valueOf() / 1000.0;
40
+ // Don't save provided AVL time if it's more than 30s in the future
41
+ const futureTimeLimit = now + 30;
42
+ // If present, replace the modified unix date with the one provided by the CAD
43
+ let movedAtUnix = now;
44
+ if (_.isString(item.avlTime) && _.trim(item.avlTime) !== "") {
45
+ const checkISODate = (new Date(item.avlTime).valueOf()) / 1000.0;
46
+ const cadUnixTime = _.isNaN(checkISODate) && _.isString(item.avlTime) ? parseFloat(item.avlTime) : checkISODate;
47
+
48
+ if (_.isFinite(cadUnixTime) && cadUnixTime <= futureTimeLimit) {
49
+ movedAtUnix = cadUnixTime;
50
+ }
51
+ }
52
+
53
+ const userId = `${item.radioName}-${item.vehicleId}`; // Fake user id radioName - vehicleId
54
+
55
+ let sharingEnabled = false;
56
+ let opAreaName = "";
57
+ let opAreaCode = "";
58
+ let deptState = "";
59
+ let locationStaleMinutes = 31 * 24 * 60; // 31d
60
+
61
+ if (_.isObject(department) && _.isObject(department.shareAVL)) {
62
+ sharingEnabled = department.shareAVL.enabled && department.accountType === "production";
63
+ opAreaName = department.shareAVL.opAreaName || "";
64
+ opAreaCode = department.shareAVL.opAreaCode || "";
65
+ }
66
+
67
+ if (_.isObject(department)) {
68
+ if (_.isObject(department.addressDetails) && _.isString(department.addressDetails.state)) {
69
+ deptState = department.addressDetails.state || "";
70
+ }
71
+ if (_.isNumber(department.locationStaleMinutes) && _.isFinite(department.locationStaleMinutes) && department.locationStaleMinutes > 0) {
72
+ locationStaleMinutes = department.locationStaleMinutes;
73
+ }
74
+ }
75
+ const deleteAfterDate = new Date(atDate.valueOf() + 1000 * 60 * locationStaleMinutes).toISOString();
76
+
77
+ const cadAVL: Partial<Location> = {
78
+ device_type: "cad",
79
+ locationGeoJSON: {
80
+ type: "Point",
81
+ coordinates: [longitude, latitude]
82
+ },
83
+ deleteAfterDate,
84
+ modified: atDate.toISOString(),
85
+ movedAt: new Date(movedAtUnix * 1000).toISOString(),
86
+ active: true,
87
+ username: item.radioName,
88
+ userId,
89
+ departmentId: department._id,
90
+ session: userId,
91
+ version: 2,
92
+ shared: sharingEnabled,
93
+ opAreaName,
94
+ opAreaCode,
95
+ state: deptState
96
+ };
97
+
98
+ const hasCoordinate = Math.abs(latitude) > 0.01 || Math.abs(longitude) > 0.01;
99
+ const validCoordinate = Math.abs(latitude) < 90 && Math.abs(longitude) < 180;
100
+ const hasVehicle = _.isString(item.vehicleId) && _.trim(item.vehicleId).length > 0;
101
+ const hasRadioName = _.isString(item.radioName) && _.trim(item.radioName).length > 0;
102
+ const isValid = hasCoordinate && validCoordinate && (hasVehicle || hasRadioName);
103
+
104
+ return {
105
+ cadAVL,
106
+ isValid,
107
+ };
108
+ }
109
+
110
+ async function processVehicle(departmentExceptions: string[], item: LocationPayload, department: Partial<Department>, atDate: Date) {
111
+ // need function to validate the item (As concerns vehicleId and radioName)
112
+ const isBidirectionalException = departmentExceptions.indexOf(department._id) !== -1;
113
+ // Determine if vehicle is active or not
114
+ const cadItem = await store.getVehicle(item, department._id);
115
+ if (!_.isObject(cadItem) &&
116
+ department.cadOneWayVehiclesEnabled &&
117
+ (department.cadBidirectionalEnabled !== true || isBidirectionalException)) {
118
+ const vehicleItem: Partial<CADVehicle> = {
119
+ departmentId: department._id,
120
+ modifiedDate: atDate.valueOf() / 1000.0,
121
+ modified: atDate.toISOString(),
122
+ radioName: item.radioName,
123
+ vehicleId: item.vehicleId,
124
+ };
125
+ await store.updateVehicle(vehicleItem, department._id);
126
+ }
127
+ return cadItem;
128
+ }
129
+
130
+ async function processItems(department: Partial<Department>, items: LocationPayload[], atDate: Date) {
131
+ for (const item of items) {
132
+ const decoded = await decodeCADVehicleStatusToLocation(item, department, atDate);
133
+ debug(`processCADVehicleLocation item: ${JSON.stringify(item)} d:${JSON.stringify(department)} at: ${atDate.toISOString()}.`);
134
+ if (!decoded.isValid) {
135
+ return;
136
+ }
137
+
138
+ let active = true;
139
+ const cadBidirectionalExceptions = [
140
+ "627d718b8d6145748b30282b" // HMT (2 way comments, 1 way vehicles)
141
+ ];
142
+
143
+ const cadItem = await processVehicle(cadBidirectionalExceptions, item, department, atDate);
144
+
145
+ if (_.isObject(cadItem) && _.isBoolean(cadItem.mapHidden)) {
146
+ active = !cadItem.mapHidden;
147
+ }
148
+
149
+ decoded.cadAVL.active = active;
150
+ return await store.updateLocation(decoded.cadAVL, atDate);
151
+ }
152
+ }
153
+
154
+ return {
155
+ processVehicle,
156
+ processItems,
157
+ };
158
+ }
159
+
160
+ export default location;
161
+ export type LocationModule = ReturnType<typeof location>;
package/src/store.ts ADDED
@@ -0,0 +1,133 @@
1
+ import debugModule from "debug";
2
+ import {
3
+ DepartmentModel,
4
+ Department,
5
+ Location,
6
+ LocationModel,
7
+ CADVehicleModel,
8
+ CADVehicle,
9
+ } from "tabletcommand-backend-models";
10
+ import _ from "lodash";
11
+ import { LocationDelta, LocationObject, LocationPayload, Vehicle } 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>;