tabletcommand-location 2.4.1 → 2.4.3
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/.claude/worktrees/sc-36275/.buildkite/pipeline.yml +5 -0
- package/.claude/worktrees/sc-36275/README.md +3 -0
- package/.claude/worktrees/sc-36275/build/audience.js +38 -0
- package/.claude/worktrees/sc-36275/build/audience.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/index.js +95 -0
- package/.claude/worktrees/sc-36275/build/index.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/location.js +278 -0
- package/.claude/worktrees/sc-36275/build/location.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/query.js +378 -0
- package/.claude/worktrees/sc-36275/build/query.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/store.js +182 -0
- package/.claude/worktrees/sc-36275/build/store.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/test/location.js +522 -0
- package/.claude/worktrees/sc-36275/build/test/location.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/test/mock.js +165 -0
- package/.claude/worktrees/sc-36275/build/test/mock.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/test/query.js +174 -0
- package/.claude/worktrees/sc-36275/build/test/query.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/types.js +8 -0
- package/.claude/worktrees/sc-36275/build/types.js.map +1 -0
- package/.claude/worktrees/sc-36275/cspell.json +64 -0
- package/.claude/worktrees/sc-36275/definitions/audience.d.ts +18 -0
- package/.claude/worktrees/sc-36275/definitions/audience.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/index.d.ts +11 -0
- package/.claude/worktrees/sc-36275/definitions/index.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/location.d.ts +32 -0
- package/.claude/worktrees/sc-36275/definitions/location.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/query.d.ts +36 -0
- package/.claude/worktrees/sc-36275/definitions/query.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/store.d.ts +64 -0
- package/.claude/worktrees/sc-36275/definitions/store.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/test/location.d.ts +2 -0
- package/.claude/worktrees/sc-36275/definitions/test/location.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/test/mock.d.ts +346 -0
- package/.claude/worktrees/sc-36275/definitions/test/mock.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/test/query.d.ts +2 -0
- package/.claude/worktrees/sc-36275/definitions/test/query.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/types.d.ts +53 -0
- package/.claude/worktrees/sc-36275/definitions/types.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/eslint.config.mjs +95 -0
- package/.claude/worktrees/sc-36275/gulpfile.js +23 -0
- package/.claude/worktrees/sc-36275/package-lock.json +9404 -0
- package/.claude/worktrees/sc-36275/package.json +54 -0
- package/.claude/worktrees/sc-36275/src/audience.ts +54 -0
- package/.claude/worktrees/sc-36275/src/index.ts +81 -0
- package/.claude/worktrees/sc-36275/src/location.ts +315 -0
- package/.claude/worktrees/sc-36275/src/query.ts +497 -0
- package/.claude/worktrees/sc-36275/src/store.ts +204 -0
- package/.claude/worktrees/sc-36275/src/test/location.ts +485 -0
- package/.claude/worktrees/sc-36275/src/test/mock.ts +186 -0
- package/.claude/worktrees/sc-36275/src/test/query.ts +209 -0
- package/.claude/worktrees/sc-36275/src/tsconfig.json +30 -0
- package/.claude/worktrees/sc-36275/src/types.ts +74 -0
- package/.claude/worktrees/sc-36275/test.sh +32 -0
- package/build/query.js +6 -1
- package/build/query.js.map +1 -1
- package/build/test/query.js +2 -2
- package/build/test/query.js.map +1 -1
- package/cspell.json +1 -0
- package/definitions/query.d.ts.map +1 -1
- package/definitions/test/mock.d.ts +5 -0
- package/definitions/test/mock.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/query.ts +6 -1
- package/src/test/query.ts +2 -2
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tabletcommand-location",
|
|
3
|
+
"version": "2.4.3",
|
|
4
|
+
"description": "Tablet Command Location",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "gulp build",
|
|
8
|
+
"lint": "eslint src",
|
|
9
|
+
"test": "type-coverage && tsx --test --test-concurrency=1 --test-reporter=spec src/test/*.ts"
|
|
10
|
+
},
|
|
11
|
+
"author": "Joe Goeman <joe@tabletcommand.com>",
|
|
12
|
+
"license": "UNLICENSED",
|
|
13
|
+
"types": "definitions/index.d.ts",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"debug": "^4.4.3",
|
|
16
|
+
"lodash": "~4.18.1",
|
|
17
|
+
"tabletcommand-backend-models": "~7.4.112"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"mongoose": "^8.24.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/eslintrc": "^3.3.6",
|
|
24
|
+
"@eslint/js": "^9.39.5",
|
|
25
|
+
"@types/chai": "^5.2.3",
|
|
26
|
+
"@types/debug": "^4.1.13",
|
|
27
|
+
"@types/express": "^4.17.25",
|
|
28
|
+
"@types/lodash": "^4.17.25",
|
|
29
|
+
"@types/node": "^24.13.3",
|
|
30
|
+
"@typescript-eslint/eslint-plugin": "^8.69.0",
|
|
31
|
+
"@typescript-eslint/parser": "^8.69.0",
|
|
32
|
+
"chai": "^6.2.2",
|
|
33
|
+
"cspell": "^10.2.0",
|
|
34
|
+
"del": "^8.0.1",
|
|
35
|
+
"eslint": "^9.39.5",
|
|
36
|
+
"eslint-plugin-promise": "^7.3.0",
|
|
37
|
+
"eslint-plugin-security": "^4.0.1",
|
|
38
|
+
"globals": "^17.11.0",
|
|
39
|
+
"gulp": "^5.0.1",
|
|
40
|
+
"gulp-execa": "^8.0.1",
|
|
41
|
+
"mongoose": "^8.24.4",
|
|
42
|
+
"neostandard": "^0.13.0",
|
|
43
|
+
"tsx": "^4.23.13",
|
|
44
|
+
"type-coverage": "^2.30.1",
|
|
45
|
+
"typescript": "~5.9.3"
|
|
46
|
+
},
|
|
47
|
+
"typeCoverage": {
|
|
48
|
+
"atLeast": 99.3,
|
|
49
|
+
"ignoreCatch": true,
|
|
50
|
+
"ignoreFiles": [
|
|
51
|
+
"src/test/**/*.ts"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccountIndustry,
|
|
3
|
+
} from "tabletcommand-backend-models";
|
|
4
|
+
|
|
5
|
+
export type AvlAudienceKind = "always" | "near" | "geo";
|
|
6
|
+
|
|
7
|
+
export interface AvlNearArea {
|
|
8
|
+
/** Radius around an anchor incident. Per-pair — deliberately not a shared constant. */
|
|
9
|
+
radiusMeters: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type AvlAudienceEntry =
|
|
13
|
+
| { kind: "always" }
|
|
14
|
+
| { kind: "near"; area: AvlNearArea } // area required exactly when near
|
|
15
|
+
| { kind: "geo" }; // geometry comes from the OWNER's shareAVL.area
|
|
16
|
+
|
|
17
|
+
/** owner industry -> reader industry -> entry, or null for denied. Every pair present. */
|
|
18
|
+
export type AvlAudienceMatrix = Record<AccountIndustry, Record<AccountIndustry, AvlAudienceEntry | null>>;
|
|
19
|
+
|
|
20
|
+
const ALWAYS: AvlAudienceEntry = { kind: "always" };
|
|
21
|
+
// eslint-ignore-next-line
|
|
22
|
+
// const GEO: AvlAudienceEntry = { kind: "geo" };
|
|
23
|
+
const DENY = null;
|
|
24
|
+
const NEAR: AvlAudienceEntry = { kind: "near", area: { radiusMeters: 5000 } }; // 5km ~ 3.1mi
|
|
25
|
+
|
|
26
|
+
export const AVL_AUDIENCE_MATRIX: AvlAudienceMatrix = {
|
|
27
|
+
// owner => reader
|
|
28
|
+
[AccountIndustry.Fire]: {
|
|
29
|
+
[AccountIndustry.Fire]: ALWAYS,
|
|
30
|
+
[AccountIndustry.Law]: ALWAYS,
|
|
31
|
+
[AccountIndustry.Campus]: DENY,
|
|
32
|
+
[AccountIndustry.Utility]: NEAR,
|
|
33
|
+
},
|
|
34
|
+
[AccountIndustry.Law]: {
|
|
35
|
+
[AccountIndustry.Fire]: NEAR,
|
|
36
|
+
[AccountIndustry.Law]: ALWAYS,
|
|
37
|
+
[AccountIndustry.Campus]: DENY,
|
|
38
|
+
[AccountIndustry.Utility]: DENY,
|
|
39
|
+
},
|
|
40
|
+
[AccountIndustry.Campus]: {
|
|
41
|
+
[AccountIndustry.Fire]: ALWAYS,
|
|
42
|
+
[AccountIndustry.Law]: ALWAYS,
|
|
43
|
+
[AccountIndustry.Campus]: DENY, // deliberate — see above
|
|
44
|
+
[AccountIndustry.Utility]: DENY,
|
|
45
|
+
},
|
|
46
|
+
[AccountIndustry.Utility]: {
|
|
47
|
+
[AccountIndustry.Fire]: NEAR,
|
|
48
|
+
[AccountIndustry.Law]: DENY,
|
|
49
|
+
[AccountIndustry.Campus]: DENY,
|
|
50
|
+
[AccountIndustry.Utility]: ALWAYS,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Unused/Incomplete
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import locationModule from "./location";
|
|
2
|
+
import storeModule from "./store";
|
|
3
|
+
import {
|
|
4
|
+
DepartmentModel,
|
|
5
|
+
LocationModel,
|
|
6
|
+
CADVehicleModel,
|
|
7
|
+
DeviceMappingModel,
|
|
8
|
+
Department,
|
|
9
|
+
} from "tabletcommand-backend-models";
|
|
10
|
+
import _ from "lodash";
|
|
11
|
+
import {
|
|
12
|
+
LocationKindType,
|
|
13
|
+
LocationPayload,
|
|
14
|
+
LocationSource,
|
|
15
|
+
LocationVisibility,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
// Export types to external projects
|
|
19
|
+
export {
|
|
20
|
+
LocationKindType,
|
|
21
|
+
LocationPayload,
|
|
22
|
+
LocationSource,
|
|
23
|
+
LocationVisibility,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// When used externally
|
|
27
|
+
// import { query } from "tabletcommand-location";
|
|
28
|
+
// query.adminListLocations({ departmentId, movedAt });
|
|
29
|
+
export * as query from "./query";
|
|
30
|
+
|
|
31
|
+
export function lib(
|
|
32
|
+
departmentModel: DepartmentModel,
|
|
33
|
+
locationModel: LocationModel,
|
|
34
|
+
cadVehicleModel: CADVehicleModel,
|
|
35
|
+
deviceMappingModel: DeviceMappingModel
|
|
36
|
+
) {
|
|
37
|
+
const store = storeModule(departmentModel, locationModel, cadVehicleModel, deviceMappingModel);
|
|
38
|
+
const location = locationModule(store);
|
|
39
|
+
|
|
40
|
+
async function processLocations(department: Partial<Department>, items: LocationPayload[] | null | undefined) {
|
|
41
|
+
const atDate = new Date();
|
|
42
|
+
try {
|
|
43
|
+
if (_.isUndefined(items) || _.isNull(items)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
if (_.isObject(items) && !_.isArray(items)) {
|
|
47
|
+
items = [items];
|
|
48
|
+
}
|
|
49
|
+
const result = await location.processItems(department, items, atDate);
|
|
50
|
+
return result;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.log("err processLocations: ", err);
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function processLocationsDeviceMapping(department: Partial<Department>, items: LocationPayload[] | null | undefined) {
|
|
58
|
+
const atDate = new Date();
|
|
59
|
+
try {
|
|
60
|
+
if (_.isUndefined(items) || _.isNull(items)) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
if (_.isObject(items) && !_.isArray(items)) {
|
|
64
|
+
items = [items];
|
|
65
|
+
}
|
|
66
|
+
const result = await location.processItemsDeviceMappingGeotab(department, items, atDate);
|
|
67
|
+
return result;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log("err processLocations: ", err);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
processLocations,
|
|
76
|
+
processLocationsDeviceMapping,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default lib;
|
|
81
|
+
export type LibModule = ReturnType<typeof lib>;
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import {
|
|
3
|
+
AccountIndustry,
|
|
4
|
+
Department,
|
|
5
|
+
Location,
|
|
6
|
+
CADVehicle,
|
|
7
|
+
LocationKindType,
|
|
8
|
+
LocationVisibility,
|
|
9
|
+
} from "tabletcommand-backend-models";
|
|
10
|
+
import {
|
|
11
|
+
LocationPayload,
|
|
12
|
+
DecodedLocation,
|
|
13
|
+
} from "./types";
|
|
14
|
+
import debugModule from "debug";
|
|
15
|
+
import { StoreModule } from "./store";
|
|
16
|
+
|
|
17
|
+
export function location(store: StoreModule) {
|
|
18
|
+
const debug = debugModule("tabletcommand-location:location");
|
|
19
|
+
|
|
20
|
+
function decodeLocation(item: LocationPayload, department: Partial<Department>, atDate: Date, deviceType: string): DecodedLocation {
|
|
21
|
+
let latitude = 0;
|
|
22
|
+
let longitude = 0;
|
|
23
|
+
|
|
24
|
+
if (_.isFinite(parseFloat(item.latitude.toString()))) {
|
|
25
|
+
latitude = parseFloat(item.latitude.toString());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (_.isFinite(parseFloat(item.longitude.toString()))) {
|
|
29
|
+
longitude = parseFloat(item.longitude.toString());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!_.isString(item.radioName)) {
|
|
33
|
+
item.radioName = "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!_.isString(item.vehicleId)) {
|
|
37
|
+
item.vehicleId = "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const now = atDate.valueOf() / 1000.0;
|
|
41
|
+
// Don't save provided AVL time if it's more than 30s in the future
|
|
42
|
+
const futureTimeLimit = now + 30;
|
|
43
|
+
// If present, replace the modified unix date with the one provided by the CAD
|
|
44
|
+
let movedAtUnix = now;
|
|
45
|
+
if (_.isString(item.avlTime) && _.trim(item.avlTime) !== "") {
|
|
46
|
+
const checkISODate = (new Date(item.avlTime).valueOf()) / 1000.0;
|
|
47
|
+
const cadUnixTime = _.isNaN(checkISODate) && _.isString(item.avlTime) ? parseFloat(item.avlTime) : checkISODate;
|
|
48
|
+
|
|
49
|
+
if (_.isFinite(cadUnixTime) && cadUnixTime <= futureTimeLimit) {
|
|
50
|
+
movedAtUnix = cadUnixTime;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Originally always was this fake id, but now can override these values
|
|
55
|
+
// If overridden then we use the values on the record instead of the fake userId
|
|
56
|
+
let userId = `${item.radioName}-${item.vehicleId}`; // Fake user id radioName - vehicleId
|
|
57
|
+
if (_.isString(item.userId) && _.trim(item.userId) !== "") {
|
|
58
|
+
userId = item.userId;
|
|
59
|
+
}
|
|
60
|
+
let session = userId;
|
|
61
|
+
if (_.isString(item.session) && _.trim(item.session) !== "") {
|
|
62
|
+
session = item.session;
|
|
63
|
+
}
|
|
64
|
+
let username = item.radioName;
|
|
65
|
+
if (_.isString(item.username) && _.trim(item.username) !== "") {
|
|
66
|
+
username = item.username;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let sharingEnabled = false;
|
|
70
|
+
let opAreaName = "";
|
|
71
|
+
let opAreaCode = "";
|
|
72
|
+
let deptState = "";
|
|
73
|
+
let locationStaleMinutes = 31 * 24 * 60; // 31d
|
|
74
|
+
|
|
75
|
+
if (_.isObject(department) && _.isObject(department.shareAVL)) {
|
|
76
|
+
sharingEnabled = department.shareAVL.enabled && department.accountType === "production";
|
|
77
|
+
opAreaName = department.shareAVL.opAreaName || "";
|
|
78
|
+
opAreaCode = department.shareAVL.opAreaCode || "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (_.isObject(department)) {
|
|
82
|
+
if (_.isObject(department.addressDetails) && _.isString(department.addressDetails.state)) {
|
|
83
|
+
deptState = department.addressDetails.state || "";
|
|
84
|
+
}
|
|
85
|
+
if (_.isNumber(department.locationStaleMinutes) && _.isFinite(department.locationStaleMinutes) && department.locationStaleMinutes > 0) {
|
|
86
|
+
locationStaleMinutes = department.locationStaleMinutes;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const deleteAfterDate = new Date(atDate.valueOf() + 1000 * 60 * locationStaleMinutes);
|
|
90
|
+
const personnelUUID = _.isString(item.personnelUUID) ? item.personnelUUID : "";
|
|
91
|
+
const transponderUUID = _.isString(item.transponderUUID) ? item.transponderUUID : "";
|
|
92
|
+
const industry: AccountIndustry = department?.industry ?? AccountIndustry.Fire; // If not set, fallback to fire
|
|
93
|
+
|
|
94
|
+
const cadAVL: Partial<Location> = {
|
|
95
|
+
device_type: deviceType,
|
|
96
|
+
source: item.source,
|
|
97
|
+
kindType: item.kindType,
|
|
98
|
+
locationGeoJSON: {
|
|
99
|
+
type: "Point",
|
|
100
|
+
coordinates: [longitude, latitude]
|
|
101
|
+
},
|
|
102
|
+
deleteAfterDate,
|
|
103
|
+
modified: atDate,
|
|
104
|
+
movedAt: new Date(movedAtUnix * 1000),
|
|
105
|
+
active: true,
|
|
106
|
+
username,
|
|
107
|
+
userId,
|
|
108
|
+
departmentId: String(department._id),
|
|
109
|
+
session,
|
|
110
|
+
version: 2,
|
|
111
|
+
shared: sharingEnabled,
|
|
112
|
+
sendToCAD: false,
|
|
113
|
+
opAreaName,
|
|
114
|
+
opAreaCode,
|
|
115
|
+
state: deptState,
|
|
116
|
+
visibility: [], // will be updated after .active is set
|
|
117
|
+
personnelUUID,
|
|
118
|
+
transponderUUID,
|
|
119
|
+
industry,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (_.isNumber(item.speed)) {
|
|
123
|
+
const speed = item.speed;
|
|
124
|
+
cadAVL.speed = speed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (_.isNumber(item.heading) && item.heading >= 0 && item.heading <= 360) {
|
|
128
|
+
const heading = item.heading;
|
|
129
|
+
cadAVL.heading = heading;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (_.isNumber(item.altitude)) {
|
|
133
|
+
const altitude = item.altitude;
|
|
134
|
+
cadAVL.altitude = altitude;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (_.isString(item.kind)) {
|
|
138
|
+
cadAVL.kind = item.kind;
|
|
139
|
+
}
|
|
140
|
+
if (_.isString(item.type)) {
|
|
141
|
+
cadAVL.type = item.type;
|
|
142
|
+
}
|
|
143
|
+
if (_.isString(item.locationType)) {
|
|
144
|
+
cadAVL.locationType = item.locationType;
|
|
145
|
+
}
|
|
146
|
+
if (_.isObject(item.kindColor) &&
|
|
147
|
+
_.isString(item.kindColor.background) && item.kindColor.background !== "" &&
|
|
148
|
+
_.isString(item.kindColor.text) && item.kindColor.text !== "") {
|
|
149
|
+
cadAVL.kindColor = item.kindColor;
|
|
150
|
+
cadAVL.kindColorChangedAt = atDate;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const hasCoordinate = Math.abs(latitude) > 0.01 || Math.abs(longitude) > 0.01;
|
|
154
|
+
const validCoordinate = Math.abs(latitude) < 90 && Math.abs(longitude) < 180;
|
|
155
|
+
const hasVehicle = _.isString(item.vehicleId) && _.trim(item.vehicleId).length > 0;
|
|
156
|
+
const hasRadioName = _.isString(item.radioName) && _.trim(item.radioName).length > 0;
|
|
157
|
+
const isValid = hasCoordinate && validCoordinate && (hasVehicle || hasRadioName);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
cadAVL,
|
|
161
|
+
isValid,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function processVehicle(item: LocationPayload, department: Partial<Department>, atDate: Date) {
|
|
166
|
+
const departmentId = String(department._id);
|
|
167
|
+
// need function to validate the item (As concerns vehicleId and radioName)
|
|
168
|
+
const isBidirectionalException = department.cadBidirectionalException || false;
|
|
169
|
+
// Determine if vehicle is active or not
|
|
170
|
+
const cadItem = await store.getVehicle(item, departmentId);
|
|
171
|
+
if (!_.isObject(cadItem) &&
|
|
172
|
+
department.cadOneWayVehiclesEnabled &&
|
|
173
|
+
(department.cadBidirectionalEnabled !== true || isBidirectionalException)) {
|
|
174
|
+
const vehicleItem: Partial<CADVehicle> = {
|
|
175
|
+
departmentId,
|
|
176
|
+
modifiedDate: atDate.valueOf() / 1000.0,
|
|
177
|
+
modified: atDate,
|
|
178
|
+
radioName: item.radioName,
|
|
179
|
+
vehicleId: item.vehicleId,
|
|
180
|
+
source: item.source,
|
|
181
|
+
};
|
|
182
|
+
await store.updateVehicle(vehicleItem, departmentId);
|
|
183
|
+
}
|
|
184
|
+
return cadItem;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Returns true when the item is a person location and was fully handled here
|
|
188
|
+
// Returns false if trying to handle vehicle item
|
|
189
|
+
async function processPersonItem(decoded: DecodedLocation, item: LocationPayload, atDate: Date): Promise<boolean> {
|
|
190
|
+
const isPerson = decoded.cadAVL.kindType === LocationKindType.Person ||
|
|
191
|
+
decoded.cadAVL.locationType === LocationKindType.Person;
|
|
192
|
+
if (!isPerson) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const hasPersonnelUUID = _.isString(decoded.cadAVL.personnelUUID) && decoded.cadAVL.personnelUUID !== "";
|
|
196
|
+
const hasTransponderUUID = _.isString(decoded.cadAVL.transponderUUID) && decoded.cadAVL.transponderUUID !== "";
|
|
197
|
+
if (!hasPersonnelUUID && !hasTransponderUUID) {
|
|
198
|
+
// A person Location with neither personnelUUID nor transponderUUID
|
|
199
|
+
// would leak into the vehicle sync endpoints (they filter on
|
|
200
|
+
// personnelUUID: "") so to process person must have one or other
|
|
201
|
+
debug(`skipping person location without personnelUUID/transponderUUID: ${JSON.stringify(item)}`);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
// Person records: no CAD vehicle, always active/visible, never shared or CAD.
|
|
205
|
+
// Stamp kindType so records classified by locationType alone still match
|
|
206
|
+
// the person feed query (kindType: "person")
|
|
207
|
+
decoded.cadAVL.kindType = LocationKindType.Person;
|
|
208
|
+
decoded.cadAVL.active = true;
|
|
209
|
+
decoded.cadAVL.shared = false;
|
|
210
|
+
decoded.cadAVL.sendToCAD = false;
|
|
211
|
+
decoded.cadAVL.visibility = [LocationVisibility.Visible];
|
|
212
|
+
await store.updateLocation(decoded.cadAVL, atDate);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function processItems(department: Partial<Department>, items: LocationPayload[], atDate: Date) {
|
|
217
|
+
for (const item of items) {
|
|
218
|
+
const decoded = decodeLocation(item, department, atDate, "cad");
|
|
219
|
+
debug(`processCADVehicleLocation item: ${JSON.stringify(item)} d:${JSON.stringify(department)} at: ${atDate.toISOString()}.`);
|
|
220
|
+
if (!decoded.isValid) {
|
|
221
|
+
// Skip only the invalid item, not the rest of the batch
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (await processPersonItem(decoded, item, atDate)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let active = true;
|
|
230
|
+
const cadItem = await processVehicle(item, department, atDate);
|
|
231
|
+
|
|
232
|
+
if (_.isObject(cadItem)) {
|
|
233
|
+
if (_.isBoolean(cadItem.mapHidden)) {
|
|
234
|
+
active = !cadItem.mapHidden;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// TODO: add support for location .kindType
|
|
238
|
+
|
|
239
|
+
decoded.cadAVL.active = active;
|
|
240
|
+
// Calculate .visibility after .active is set
|
|
241
|
+
const visibility: LocationVisibility[] = [
|
|
242
|
+
active ? LocationVisibility.Visible : LocationVisibility.Hidden,
|
|
243
|
+
];
|
|
244
|
+
if (decoded.cadAVL.shared) {
|
|
245
|
+
visibility.push(LocationVisibility.Shared);
|
|
246
|
+
}
|
|
247
|
+
decoded.cadAVL.visibility = visibility;
|
|
248
|
+
await store.updateLocation(decoded.cadAVL, atDate);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function processItemsDeviceMappingGeotab(department: Partial<Department>, items: LocationPayload[], atDate: Date) {
|
|
253
|
+
const departmentId = String(department._id);
|
|
254
|
+
for (const item of items) {
|
|
255
|
+
// TODO: may need to break decodeLocation out separately for device mapping, but works right now
|
|
256
|
+
const decoded = decodeLocation(item, department, atDate, "geotab");
|
|
257
|
+
debug(`processItemsDeviceMappingGeotab item: ${JSON.stringify(item)} d:${JSON.stringify(department)} at: ${atDate.toISOString()}.`);
|
|
258
|
+
if (!decoded.isValid) {
|
|
259
|
+
// Skip only the invalid item, not the rest of the batch
|
|
260
|
+
// This is actually a fix for vehicle processing as well
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Defensive code technically we shouldn't be getting here with person
|
|
265
|
+
// But it would still be valid to use this with a person so added the logic
|
|
266
|
+
if (await processPersonItem(decoded, item, atDate)) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let active = true;
|
|
271
|
+
let locationToCAD = false;
|
|
272
|
+
const deviceMapping = await store.getDeviceMappingGeotab(item, departmentId);
|
|
273
|
+
|
|
274
|
+
if (_.isObject(deviceMapping)) {
|
|
275
|
+
if (_.isBoolean(deviceMapping.mapHidden)) {
|
|
276
|
+
active = !deviceMapping.mapHidden;
|
|
277
|
+
}
|
|
278
|
+
if (_.isBoolean(deviceMapping.locationToCAD)) {
|
|
279
|
+
locationToCAD = true;
|
|
280
|
+
}
|
|
281
|
+
if (_.isString(deviceMapping.mapId) && deviceMapping.mapId.trim() !== "") {
|
|
282
|
+
const radioName = deviceMapping.mapId;
|
|
283
|
+
const userId = `${radioName}-${item.vehicleId}`; // Fake user id radioName - vehicleId
|
|
284
|
+
decoded.cadAVL.username = radioName;
|
|
285
|
+
decoded.cadAVL.userId = userId;
|
|
286
|
+
decoded.cadAVL.session = userId;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// TODO: add support for location .kindType
|
|
290
|
+
decoded.cadAVL.active = active;
|
|
291
|
+
// Calculate .visibility after .active is set
|
|
292
|
+
const visibility: LocationVisibility[] = [
|
|
293
|
+
active ? LocationVisibility.Visible : LocationVisibility.Hidden,
|
|
294
|
+
];
|
|
295
|
+
if (decoded.cadAVL.shared) {
|
|
296
|
+
visibility.push(LocationVisibility.Shared);
|
|
297
|
+
}
|
|
298
|
+
if (locationToCAD) {
|
|
299
|
+
decoded.cadAVL.sendToCAD = true;
|
|
300
|
+
visibility.push(LocationVisibility.CAD);
|
|
301
|
+
}
|
|
302
|
+
decoded.cadAVL.visibility = visibility;
|
|
303
|
+
await store.updateLocation(decoded.cadAVL, atDate);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
processVehicle,
|
|
309
|
+
processItems,
|
|
310
|
+
processItemsDeviceMappingGeotab,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export default location;
|
|
315
|
+
export type LocationModule = ReturnType<typeof location>;
|