tabletcommand-location 2.4.0 → 2.4.2
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/build/audience.js +38 -0
- package/build/audience.js.map +1 -0
- package/build/index.js +38 -20
- package/build/index.js.map +1 -1
- package/build/location.js +3 -0
- package/build/location.js.map +1 -1
- package/build/query.js +378 -0
- package/build/query.js.map +1 -0
- package/build/store.js +2 -0
- package/build/store.js.map +1 -1
- package/build/test/location.js +53 -0
- package/build/test/location.js.map +1 -1
- package/build/test/mock.js +5 -0
- package/build/test/mock.js.map +1 -1
- package/build/test/query.js +174 -0
- package/build/test/query.js.map +1 -0
- package/cspell.json +2 -1
- package/definitions/audience.d.ts +18 -0
- package/definitions/audience.d.ts.map +1 -0
- package/definitions/index.d.ts +1 -0
- package/definitions/index.d.ts.map +1 -1
- package/definitions/location.d.ts.map +1 -1
- package/definitions/query.d.ts +36 -0
- package/definitions/query.d.ts.map +1 -0
- package/definitions/store.d.ts +5 -0
- package/definitions/store.d.ts.map +1 -1
- package/definitions/test/mock.d.ts +5 -0
- package/definitions/test/mock.d.ts.map +1 -1
- package/definitions/test/query.d.ts +2 -0
- package/definitions/test/query.d.ts.map +1 -0
- package/package.json +18 -14
- package/src/audience.ts +54 -0
- package/src/index.ts +5 -20
- package/src/location.ts +3 -0
- package/src/query.ts +497 -0
- package/src/store.ts +2 -0
- package/src/test/location.ts +53 -1
- package/src/test/mock.ts +6 -0
- package/src/test/query.ts +209 -0
- package/test.sh +1 -1
package/src/audience.ts
ADDED
|
@@ -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
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,11 @@ export {
|
|
|
23
23
|
LocationVisibility,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// When used externally
|
|
27
|
+
// import { query } from "tabletcommand-location";
|
|
28
|
+
// query.adminListLocations({ departmentId, movedAt });
|
|
29
|
+
export * as query from "./query";
|
|
30
|
+
|
|
26
31
|
export function lib(
|
|
27
32
|
departmentModel: DepartmentModel,
|
|
28
33
|
locationModel: LocationModel,
|
|
@@ -31,9 +36,6 @@ export function lib(
|
|
|
31
36
|
) {
|
|
32
37
|
const store = storeModule(departmentModel, locationModel, cadVehicleModel, deviceMappingModel);
|
|
33
38
|
const location = locationModule(store);
|
|
34
|
-
// async function processVehicle(department: Partial<Department>, item: Partial<CADVehicle>) {
|
|
35
|
-
// // await location.processVehicle()
|
|
36
|
-
// }
|
|
37
39
|
|
|
38
40
|
async function processLocations(department: Partial<Department>, items: LocationPayload[] | null | undefined) {
|
|
39
41
|
const atDate = new Date();
|
|
@@ -69,26 +71,9 @@ export function lib(
|
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
// async function processLocationsOnly(department: Partial<Department>, items: Partial<Location>[] | null | undefined, agency: Partial<Agency>) {
|
|
73
|
-
// try {
|
|
74
|
-
// if (_.isUndefined(items) || _.isNull(items)) {
|
|
75
|
-
// return [];
|
|
76
|
-
// }
|
|
77
|
-
// if (_.isObject(items) && !_.isArray(items)) {
|
|
78
|
-
// items = [items];
|
|
79
|
-
// }
|
|
80
|
-
// const result = await location.processItemsNoVehicle(department, items, agency);
|
|
81
|
-
// return result;
|
|
82
|
-
// } catch (err) {
|
|
83
|
-
// throw err;
|
|
84
|
-
// }
|
|
85
|
-
// }
|
|
86
|
-
|
|
87
74
|
return {
|
|
88
|
-
// processVehicle,
|
|
89
75
|
processLocations,
|
|
90
76
|
processLocationsDeviceMapping,
|
|
91
|
-
// processLocationsOnly,
|
|
92
77
|
};
|
|
93
78
|
}
|
|
94
79
|
|
package/src/location.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import _ from "lodash";
|
|
2
2
|
import {
|
|
3
|
+
AccountIndustry,
|
|
3
4
|
Department,
|
|
4
5
|
Location,
|
|
5
6
|
CADVehicle,
|
|
@@ -88,6 +89,7 @@ export function location(store: StoreModule) {
|
|
|
88
89
|
const deleteAfterDate = new Date(atDate.valueOf() + 1000 * 60 * locationStaleMinutes);
|
|
89
90
|
const personnelUUID = _.isString(item.personnelUUID) ? item.personnelUUID : "";
|
|
90
91
|
const transponderUUID = _.isString(item.transponderUUID) ? item.transponderUUID : "";
|
|
92
|
+
const industry: AccountIndustry = department?.industry ?? AccountIndustry.Fire; // If not set, fallback to fire
|
|
91
93
|
|
|
92
94
|
const cadAVL: Partial<Location> = {
|
|
93
95
|
device_type: deviceType,
|
|
@@ -114,6 +116,7 @@ export function location(store: StoreModule) {
|
|
|
114
116
|
visibility: [], // will be updated after .active is set
|
|
115
117
|
personnelUUID,
|
|
116
118
|
transponderUUID,
|
|
119
|
+
industry,
|
|
117
120
|
};
|
|
118
121
|
|
|
119
122
|
if (_.isNumber(item.speed)) {
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FilterQuery,
|
|
3
|
+
ProjectionType,
|
|
4
|
+
SortOrder,
|
|
5
|
+
} from "mongoose";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Location,
|
|
9
|
+
} from "tabletcommand-backend-models";
|
|
10
|
+
|
|
11
|
+
export type MQP = {
|
|
12
|
+
list: FilterQuery<Location>,
|
|
13
|
+
projection: ProjectionType<Location>,
|
|
14
|
+
sortBy?: Record<string, SortOrder>,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type AdminListLocationParams1 = {
|
|
18
|
+
departmentId: string,
|
|
19
|
+
movedAt: Date,
|
|
20
|
+
showAll?: boolean,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type AdminListLocationParams2 = {
|
|
24
|
+
departmentId: string,
|
|
25
|
+
movedAt: Date,
|
|
26
|
+
longitude: number,
|
|
27
|
+
latitude: number,
|
|
28
|
+
diagonal: number,
|
|
29
|
+
showAll?: boolean,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type SyncModifiedDate = {
|
|
33
|
+
modifiedDate: Date,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const metersToRadians = 6378100; // The equatorial radius of the Earth in meters
|
|
37
|
+
|
|
38
|
+
function centerSphereFilter(longitude: number, latitude: number, diagonal: number) {
|
|
39
|
+
return {
|
|
40
|
+
$geoWithin: {
|
|
41
|
+
$centerSphere: [
|
|
42
|
+
[longitude, latitude],
|
|
43
|
+
diagonal / metersToRadians,
|
|
44
|
+
] as [[number, number], number],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Transponder-tracked person records carry an empty personnelUUID;
|
|
50
|
+
// they surface via the person/responder feeds, never in unit/vehicle lists
|
|
51
|
+
const excludePersonAndTransponderRecords = [
|
|
52
|
+
{
|
|
53
|
+
$or: [
|
|
54
|
+
{ personnelUUID: "" },
|
|
55
|
+
{ personnelUUID: { $exists: false } }, // TODO: Remove $exists check once all location services are running the latest
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
$or: [
|
|
60
|
+
{ transponderUUID: "" },
|
|
61
|
+
{ transponderUUID: { $exists: false } },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Same fields defined in the frontend
|
|
67
|
+
// api/a1/location/units and api/a1/location/units-shared
|
|
68
|
+
const projectionAdmin = {
|
|
69
|
+
_id: 1,
|
|
70
|
+
active: 1,
|
|
71
|
+
altitude: 1,
|
|
72
|
+
color: 1,
|
|
73
|
+
device_type: 1,
|
|
74
|
+
industry: 1,
|
|
75
|
+
kindType: 1,
|
|
76
|
+
location: 1,
|
|
77
|
+
locationGeoJSON: 1,
|
|
78
|
+
modified: 1,
|
|
79
|
+
movedAt: 1,
|
|
80
|
+
sendToCAD: 1,
|
|
81
|
+
source: 1,
|
|
82
|
+
speed: 1,
|
|
83
|
+
uuid: 1, // Legacy, remove later
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function adminListLocations(params: AdminListLocationParams1): MQP {
|
|
87
|
+
const {
|
|
88
|
+
departmentId,
|
|
89
|
+
movedAt,
|
|
90
|
+
showAll,
|
|
91
|
+
} = params;
|
|
92
|
+
|
|
93
|
+
const query: FilterQuery<Location> = {
|
|
94
|
+
// TODO: Replace models.Location active: true with visibility in "visible"
|
|
95
|
+
active: true,
|
|
96
|
+
departmentId,
|
|
97
|
+
$and: excludePersonAndTransponderRecords,
|
|
98
|
+
kindType: {
|
|
99
|
+
$ne: "uav",
|
|
100
|
+
},
|
|
101
|
+
// 03/14/23 (Joe) - Trying movedAt only [sc-11794] - determined modified was being overwritten by cad vehicle status updates
|
|
102
|
+
movedAt: {
|
|
103
|
+
$gte: movedAt,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
if (showAll) {
|
|
107
|
+
delete query.active;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
list: query,
|
|
112
|
+
projection: projectionAdmin,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function adminListPersonLocations(params: AdminListLocationParams1): MQP {
|
|
117
|
+
const {
|
|
118
|
+
departmentId,
|
|
119
|
+
movedAt,
|
|
120
|
+
showAll
|
|
121
|
+
} = params;
|
|
122
|
+
|
|
123
|
+
const query: FilterQuery<Location> = {
|
|
124
|
+
active: true,
|
|
125
|
+
departmentId,
|
|
126
|
+
kindType: "person",
|
|
127
|
+
movedAt: {
|
|
128
|
+
$gte: movedAt,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
if (showAll) {
|
|
132
|
+
delete query.active;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Same fields defined in the frontend
|
|
136
|
+
// api/a1/location/person
|
|
137
|
+
const projection = {
|
|
138
|
+
_id: 1,
|
|
139
|
+
active: 1,
|
|
140
|
+
altitude: 1,
|
|
141
|
+
color: 1,
|
|
142
|
+
device_type: 1,
|
|
143
|
+
industry: 1,
|
|
144
|
+
kindType: 1,
|
|
145
|
+
location: 1,
|
|
146
|
+
locationGeoJSON: 1,
|
|
147
|
+
modified: 1,
|
|
148
|
+
movedAt: 1,
|
|
149
|
+
personnelUUID: 1,
|
|
150
|
+
sendToCAD: 1,
|
|
151
|
+
source: 1,
|
|
152
|
+
speed: 1,
|
|
153
|
+
transponderUUID: 1,
|
|
154
|
+
uuid: 1, // Legacy, remove later
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
list: query,
|
|
159
|
+
projection,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function adminListUAVLocations(params: AdminListLocationParams1): MQP {
|
|
164
|
+
const {
|
|
165
|
+
departmentId,
|
|
166
|
+
movedAt,
|
|
167
|
+
showAll,
|
|
168
|
+
} = params;
|
|
169
|
+
|
|
170
|
+
const query: FilterQuery<Location> = {
|
|
171
|
+
active: true,
|
|
172
|
+
departmentId,
|
|
173
|
+
kindType: "uav",
|
|
174
|
+
movedAt: {
|
|
175
|
+
$gte: movedAt,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
if (showAll) {
|
|
179
|
+
delete query.active;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const projection = {
|
|
183
|
+
_id: 1,
|
|
184
|
+
active: 1,
|
|
185
|
+
agencyCode: 1,
|
|
186
|
+
agencyName: 1,
|
|
187
|
+
altitude: 1,
|
|
188
|
+
color: 1,
|
|
189
|
+
device_type: 1,
|
|
190
|
+
heading: 1,
|
|
191
|
+
industry: 1,
|
|
192
|
+
kindType: 1,
|
|
193
|
+
locationGeoJSON: 1,
|
|
194
|
+
modified: 1,
|
|
195
|
+
movedAt: 1,
|
|
196
|
+
opAreaCode: 1,
|
|
197
|
+
opAreaName: 1,
|
|
198
|
+
sendToCAD: 1,
|
|
199
|
+
source: 1,
|
|
200
|
+
speed: 1,
|
|
201
|
+
state: 1,
|
|
202
|
+
userId: 1,
|
|
203
|
+
username: 1,
|
|
204
|
+
uuid: 1, // Legacy, remove later
|
|
205
|
+
vehicleId: 1,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
list: query,
|
|
210
|
+
projection,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function adminListAircraftLocations(params: AdminListLocationParams2): MQP {
|
|
215
|
+
const {
|
|
216
|
+
departmentId,
|
|
217
|
+
movedAt,
|
|
218
|
+
longitude,
|
|
219
|
+
latitude,
|
|
220
|
+
diagonal
|
|
221
|
+
} = params;
|
|
222
|
+
|
|
223
|
+
const list = {
|
|
224
|
+
shared: true,
|
|
225
|
+
active: true,
|
|
226
|
+
source: "ADSB",
|
|
227
|
+
kindType: {
|
|
228
|
+
$in: ["fixed-wing", "helicopter"],
|
|
229
|
+
},
|
|
230
|
+
departmentId: {
|
|
231
|
+
$ne: departmentId,
|
|
232
|
+
},
|
|
233
|
+
movedAt: {
|
|
234
|
+
$gte: movedAt.toISOString(),
|
|
235
|
+
},
|
|
236
|
+
locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
list,
|
|
241
|
+
projection: projectionAdmin,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function adminListSharedLocations(params: AdminListLocationParams2): MQP {
|
|
246
|
+
const {
|
|
247
|
+
departmentId,
|
|
248
|
+
movedAt,
|
|
249
|
+
longitude,
|
|
250
|
+
latitude,
|
|
251
|
+
diagonal
|
|
252
|
+
} = params;
|
|
253
|
+
|
|
254
|
+
const listSharedAVLQuery = {
|
|
255
|
+
shared: true,
|
|
256
|
+
active: true,
|
|
257
|
+
source: {
|
|
258
|
+
$ne: "ADSB"
|
|
259
|
+
},
|
|
260
|
+
departmentId: {
|
|
261
|
+
$ne: departmentId,
|
|
262
|
+
},
|
|
263
|
+
kindType: {
|
|
264
|
+
$ne: "uav",
|
|
265
|
+
},
|
|
266
|
+
movedAt: {
|
|
267
|
+
$gte: movedAt.toISOString(),
|
|
268
|
+
},
|
|
269
|
+
locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
list: listSharedAVLQuery,
|
|
274
|
+
projection: projectionAdmin,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function adminListFocusedLocations(params: AdminListLocationParams2): MQP {
|
|
279
|
+
const {
|
|
280
|
+
departmentId,
|
|
281
|
+
movedAt,
|
|
282
|
+
longitude,
|
|
283
|
+
latitude,
|
|
284
|
+
diagonal,
|
|
285
|
+
showAll
|
|
286
|
+
} = params;
|
|
287
|
+
|
|
288
|
+
const listFocusedAVLQuery: FilterQuery<Location> = {
|
|
289
|
+
active: true,
|
|
290
|
+
departmentId,
|
|
291
|
+
movedAt: {
|
|
292
|
+
$gte: movedAt.toISOString(),
|
|
293
|
+
},
|
|
294
|
+
locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (showAll) {
|
|
298
|
+
delete listFocusedAVLQuery.locationGeoJSON;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
list: listFocusedAVLQuery,
|
|
303
|
+
projection: projectionAdmin,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Sync queries
|
|
308
|
+
|
|
309
|
+
const projectionSync: ProjectionType<Location> = {
|
|
310
|
+
active: 1,
|
|
311
|
+
departmentId: 1,
|
|
312
|
+
device_type: 1, // used in web ui
|
|
313
|
+
location: 1,
|
|
314
|
+
locationGeoJSON: 1,
|
|
315
|
+
modified: 1,
|
|
316
|
+
userId: 1,
|
|
317
|
+
username: 1,
|
|
318
|
+
uuid: 1, // used in iOS
|
|
319
|
+
// both used by AVL sharing
|
|
320
|
+
opAreaName: 1,
|
|
321
|
+
opAreaCode: 1,
|
|
322
|
+
shared: 1,
|
|
323
|
+
state: 1,
|
|
324
|
+
// v3
|
|
325
|
+
altitude: 1,
|
|
326
|
+
speed: 1,
|
|
327
|
+
movedAt: 1,
|
|
328
|
+
agencyName: 1,
|
|
329
|
+
agencyCode: 1,
|
|
330
|
+
// v4
|
|
331
|
+
color: 1,
|
|
332
|
+
colorChangedAt: 1,
|
|
333
|
+
propsChangedAt: 1,
|
|
334
|
+
heading: 1,
|
|
335
|
+
// v4 extra
|
|
336
|
+
kindType: 1,
|
|
337
|
+
source: 1,
|
|
338
|
+
personnelUUID: 1,
|
|
339
|
+
transponderUUID: 1,
|
|
340
|
+
// v4 kind/type/locationType
|
|
341
|
+
kind: 1,
|
|
342
|
+
type: 1,
|
|
343
|
+
locationType: 1,
|
|
344
|
+
kindColor: 1,
|
|
345
|
+
// v4
|
|
346
|
+
industry: 1,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export function syncListSharedLocations(params: AdminListLocationParams2 & SyncModifiedDate): MQP {
|
|
350
|
+
const {
|
|
351
|
+
departmentId,
|
|
352
|
+
modifiedDate,
|
|
353
|
+
movedAt,
|
|
354
|
+
longitude,
|
|
355
|
+
latitude,
|
|
356
|
+
diagonal,
|
|
357
|
+
} = params;
|
|
358
|
+
|
|
359
|
+
const list: FilterQuery<Location> = {
|
|
360
|
+
shared: true,
|
|
361
|
+
source: {
|
|
362
|
+
$nin: ["ADSB", "DroneSense"]
|
|
363
|
+
},
|
|
364
|
+
departmentId: {
|
|
365
|
+
$ne: departmentId,
|
|
366
|
+
},
|
|
367
|
+
modified: {
|
|
368
|
+
$gte: modifiedDate.toISOString(),
|
|
369
|
+
},
|
|
370
|
+
movedAt: {
|
|
371
|
+
$gte: movedAt.toISOString(),
|
|
372
|
+
},
|
|
373
|
+
locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
list,
|
|
378
|
+
projection: projectionSync,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function syncListLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
|
|
383
|
+
const {
|
|
384
|
+
departmentId,
|
|
385
|
+
modifiedDate,
|
|
386
|
+
movedAt,
|
|
387
|
+
} = params;
|
|
388
|
+
|
|
389
|
+
const list: FilterQuery<Location> = {
|
|
390
|
+
departmentId,
|
|
391
|
+
$and: excludePersonAndTransponderRecords,
|
|
392
|
+
kindType: {
|
|
393
|
+
$ne: "uav"
|
|
394
|
+
},
|
|
395
|
+
modified: {
|
|
396
|
+
$gte: modifiedDate.toISOString(),
|
|
397
|
+
},
|
|
398
|
+
movedAt: {
|
|
399
|
+
$gte: movedAt.toISOString(),
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
list,
|
|
405
|
+
projection: projectionSync,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function syncListPersonLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
|
|
410
|
+
const {
|
|
411
|
+
departmentId,
|
|
412
|
+
modifiedDate,
|
|
413
|
+
movedAt,
|
|
414
|
+
} = params;
|
|
415
|
+
|
|
416
|
+
const list: FilterQuery<Location> = {
|
|
417
|
+
departmentId,
|
|
418
|
+
kindType: "person",
|
|
419
|
+
modified: {
|
|
420
|
+
$gte: modifiedDate.toISOString(),
|
|
421
|
+
},
|
|
422
|
+
movedAt: {
|
|
423
|
+
$gte: movedAt.toISOString(),
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
list,
|
|
429
|
+
projection: projectionSync,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function syncListAircraftLocations(params: AdminListLocationParams2 & SyncModifiedDate): MQP {
|
|
434
|
+
const {
|
|
435
|
+
departmentId,
|
|
436
|
+
movedAt,
|
|
437
|
+
modifiedDate,
|
|
438
|
+
// Possibly missing
|
|
439
|
+
longitude,
|
|
440
|
+
latitude,
|
|
441
|
+
diagonal
|
|
442
|
+
} = params;
|
|
443
|
+
|
|
444
|
+
const list: FilterQuery<Location> = {
|
|
445
|
+
shared: true,
|
|
446
|
+
source: "ADSB",
|
|
447
|
+
kindType: {
|
|
448
|
+
$in: ["fixed-wing", "helicopter"]
|
|
449
|
+
},
|
|
450
|
+
departmentId: {
|
|
451
|
+
$ne: departmentId,
|
|
452
|
+
},
|
|
453
|
+
modified: {
|
|
454
|
+
$gte: modifiedDate.toISOString(),
|
|
455
|
+
},
|
|
456
|
+
movedAt: {
|
|
457
|
+
$gte: movedAt.toISOString(),
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const hasCoordinate = Math.abs(latitude) > 0.01 || Math.abs(longitude) > 0.01;
|
|
462
|
+
const validCoordinate = Math.abs(latitude) < 90 && Math.abs(longitude) < 180;
|
|
463
|
+
const filterOnLocation = hasCoordinate && validCoordinate;
|
|
464
|
+
if (filterOnLocation) {
|
|
465
|
+
list.locationGeoJSON = centerSphereFilter(longitude, latitude, diagonal);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
list,
|
|
470
|
+
projection: projectionSync,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function syncListUAVLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
|
|
475
|
+
const {
|
|
476
|
+
departmentId,
|
|
477
|
+
modifiedDate,
|
|
478
|
+
movedAt,
|
|
479
|
+
|
|
480
|
+
} = params;
|
|
481
|
+
|
|
482
|
+
const list: FilterQuery<Location> = {
|
|
483
|
+
departmentId,
|
|
484
|
+
kindType: "uav",
|
|
485
|
+
modified: {
|
|
486
|
+
$gte: modifiedDate.toISOString(),
|
|
487
|
+
},
|
|
488
|
+
movedAt: {
|
|
489
|
+
$gte: movedAt.toISOString(),
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
list,
|
|
495
|
+
projection: projectionSync,
|
|
496
|
+
};
|
|
497
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -125,6 +125,7 @@ export function store(
|
|
|
125
125
|
dbItem.locationGeoJSON = modelItem.locationGeoJSON;
|
|
126
126
|
dbItem.source = modelItem.source;
|
|
127
127
|
dbItem.kindType = modelItem.kindType;
|
|
128
|
+
dbItem.industry = modelItem.industry;
|
|
128
129
|
// Carry person/category fields on the update path; only overwrite when the
|
|
129
130
|
// incoming payload actually provides a value, so sources that don't send
|
|
130
131
|
// these fields don't wipe values written by other sources.
|
|
@@ -183,6 +184,7 @@ export function store(
|
|
|
183
184
|
"shared",
|
|
184
185
|
"state",
|
|
185
186
|
"username",
|
|
187
|
+
"industry",
|
|
186
188
|
];
|
|
187
189
|
if (_.intersection(updatedKeys, propsKeys).length > 0) {
|
|
188
190
|
locationDelta.propsChangedAt = atDate;
|
package/src/test/location.ts
CHANGED
|
@@ -5,7 +5,7 @@ import storeModule, { StoreModule } from "../store";
|
|
|
5
5
|
import mockModule from "./mock";
|
|
6
6
|
import locationModule, { LocationModule } from "../location";
|
|
7
7
|
import { LocationPayload } from "../types";
|
|
8
|
-
import { BackendModels } from "tabletcommand-backend-models";
|
|
8
|
+
import { AccountIndustry, BackendModels } from "tabletcommand-backend-models";
|
|
9
9
|
|
|
10
10
|
let location: LocationModule;
|
|
11
11
|
let store: StoreModule;
|
|
@@ -429,5 +429,57 @@ describe("location", () => {
|
|
|
429
429
|
assert.equal(recs[0]?.kindColor?.text, "#FFFFFF");
|
|
430
430
|
assert.instanceOf(recs[0]?.kindColorChangedAt, Date);
|
|
431
431
|
});
|
|
432
|
+
it("stamps the department's industry on the saved location", async () => {
|
|
433
|
+
const atDate = new Date();
|
|
434
|
+
const session = "item-with-industry-utility";
|
|
435
|
+
const items = [{
|
|
436
|
+
vehicleId: "BEEDOO1",
|
|
437
|
+
radioName: "BEEDOO1",
|
|
438
|
+
latitude: 34.35264,
|
|
439
|
+
longitude: -119.0669,
|
|
440
|
+
avlTime: "1602563494",
|
|
441
|
+
session,
|
|
442
|
+
}] as unknown as LocationPayload[];
|
|
443
|
+
await location.processItems(mockDepartment, items, atDate);
|
|
444
|
+
const sut = await models.Location.findOne({ session });
|
|
445
|
+
assert.equal(sut?.industry, AccountIndustry.Utility);
|
|
446
|
+
});
|
|
447
|
+
it("falls back to fire industry when the department has none set", async () => {
|
|
448
|
+
const atDate = new Date();
|
|
449
|
+
const session = "item-with-industry-fallback";
|
|
450
|
+
const department = { ...mockDepartment, industry: undefined };
|
|
451
|
+
const items = [{
|
|
452
|
+
vehicleId: "BEEDOO1",
|
|
453
|
+
radioName: "BEEDOO1",
|
|
454
|
+
latitude: 34.35264,
|
|
455
|
+
longitude: -119.0669,
|
|
456
|
+
avlTime: "1602563494",
|
|
457
|
+
session,
|
|
458
|
+
}] as unknown as LocationPayload[];
|
|
459
|
+
await location.processItems(department, items, atDate);
|
|
460
|
+
const sut = await models.Location.findOne({ session });
|
|
461
|
+
assert.equal(sut?.industry, AccountIndustry.Fire);
|
|
462
|
+
});
|
|
463
|
+
it("updates industry in place and stamps propsChangedAt when it changes", async () => {
|
|
464
|
+
const base = {
|
|
465
|
+
vehicleId: "BEEDOO1",
|
|
466
|
+
radioName: "BEEDOO1",
|
|
467
|
+
latitude: 34.35264,
|
|
468
|
+
longitude: -119.0669,
|
|
469
|
+
avlTime: "1602563494",
|
|
470
|
+
} as unknown as LocationPayload;
|
|
471
|
+
await location.processItems(mockDepartment, [base], new Date());
|
|
472
|
+
const initial = await models.Location.find({});
|
|
473
|
+
assert.equal(initial[0]?.industry, AccountIndustry.Utility);
|
|
474
|
+
const initialPropsChangedAt = initial[0]?.propsChangedAt;
|
|
475
|
+
assert.instanceOf(initialPropsChangedAt, Date);
|
|
476
|
+
|
|
477
|
+
const lawDepartment = { ...mockDepartment, industry: AccountIndustry.Law };
|
|
478
|
+
await location.processItems(lawDepartment, [base], new Date());
|
|
479
|
+
const recs = await models.Location.find({});
|
|
480
|
+
assert.equal(recs.length, 1);
|
|
481
|
+
assert.equal(recs[0]?.industry, AccountIndustry.Law);
|
|
482
|
+
assert.isAbove(recs[0]!.propsChangedAt.valueOf(), initialPropsChangedAt!.valueOf());
|
|
483
|
+
});
|
|
432
484
|
});
|
|
433
485
|
});
|