tabletcommand-location 2.3.8 → 2.4.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/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 +63 -6
- package/build/location.js.map +1 -1
- package/build/query.js +373 -0
- package/build/query.js.map +1 -0
- package/build/store.js +54 -5
- package/build/store.js.map +1 -1
- package/build/test/location.js +313 -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 +5 -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 +15 -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/definitions/types.d.ts +5 -0
- package/definitions/types.d.ts.map +1 -1
- package/package.json +18 -14
- package/src/audience.ts +54 -0
- package/src/index.ts +5 -20
- package/src/location.ts +67 -6
- package/src/query.ts +492 -0
- package/src/store.ts +55 -5
- package/src/test/location.ts +306 -1
- package/src/test/mock.ts +6 -0
- package/src/test/query.ts +209 -0
- package/src/types.ts +15 -0
- package/test.sh +1 -1
package/src/location.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import _ from "lodash";
|
|
2
2
|
import {
|
|
3
|
+
AccountIndustry,
|
|
3
4
|
Department,
|
|
4
5
|
Location,
|
|
5
6
|
CADVehicle,
|
|
7
|
+
LocationKindType,
|
|
6
8
|
LocationVisibility,
|
|
7
9
|
} from "tabletcommand-backend-models";
|
|
8
10
|
import {
|
|
@@ -49,7 +51,20 @@ export function location(store: StoreModule) {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
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
|
+
}
|
|
53
68
|
|
|
54
69
|
let sharingEnabled = false;
|
|
55
70
|
let opAreaName = "";
|
|
@@ -72,7 +87,9 @@ export function location(store: StoreModule) {
|
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
const deleteAfterDate = new Date(atDate.valueOf() + 1000 * 60 * locationStaleMinutes);
|
|
75
|
-
const personnelUUID = "";
|
|
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
|
|
76
93
|
|
|
77
94
|
const cadAVL: Partial<Location> = {
|
|
78
95
|
device_type: deviceType,
|
|
@@ -86,10 +103,10 @@ export function location(store: StoreModule) {
|
|
|
86
103
|
modified: atDate,
|
|
87
104
|
movedAt: new Date(movedAtUnix * 1000),
|
|
88
105
|
active: true,
|
|
89
|
-
username
|
|
106
|
+
username,
|
|
90
107
|
userId,
|
|
91
108
|
departmentId: String(department._id),
|
|
92
|
-
session
|
|
109
|
+
session,
|
|
93
110
|
version: 2,
|
|
94
111
|
shared: sharingEnabled,
|
|
95
112
|
sendToCAD: false,
|
|
@@ -98,6 +115,8 @@ export function location(store: StoreModule) {
|
|
|
98
115
|
state: deptState,
|
|
99
116
|
visibility: [], // will be updated after .active is set
|
|
100
117
|
personnelUUID,
|
|
118
|
+
transponderUUID,
|
|
119
|
+
industry,
|
|
101
120
|
};
|
|
102
121
|
|
|
103
122
|
if (_.isNumber(item.speed)) {
|
|
@@ -165,12 +184,46 @@ export function location(store: StoreModule) {
|
|
|
165
184
|
return cadItem;
|
|
166
185
|
}
|
|
167
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
|
+
|
|
168
216
|
async function processItems(department: Partial<Department>, items: LocationPayload[], atDate: Date) {
|
|
169
217
|
for (const item of items) {
|
|
170
218
|
const decoded = decodeLocation(item, department, atDate, "cad");
|
|
171
219
|
debug(`processCADVehicleLocation item: ${JSON.stringify(item)} d:${JSON.stringify(department)} at: ${atDate.toISOString()}.`);
|
|
172
220
|
if (!decoded.isValid) {
|
|
173
|
-
|
|
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;
|
|
174
227
|
}
|
|
175
228
|
|
|
176
229
|
let active = true;
|
|
@@ -203,7 +256,15 @@ export function location(store: StoreModule) {
|
|
|
203
256
|
const decoded = decodeLocation(item, department, atDate, "geotab");
|
|
204
257
|
debug(`processItemsDeviceMappingGeotab item: ${JSON.stringify(item)} d:${JSON.stringify(department)} at: ${atDate.toISOString()}.`);
|
|
205
258
|
if (!decoded.isValid) {
|
|
206
|
-
|
|
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;
|
|
207
268
|
}
|
|
208
269
|
|
|
209
270
|
let active = true;
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
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
|
+
kindType: 1,
|
|
75
|
+
location: 1,
|
|
76
|
+
locationGeoJSON: 1,
|
|
77
|
+
modified: 1,
|
|
78
|
+
movedAt: 1,
|
|
79
|
+
sendToCAD: 1,
|
|
80
|
+
source: 1,
|
|
81
|
+
speed: 1,
|
|
82
|
+
uuid: 1, // Legacy, remove later
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function adminListLocations(params: AdminListLocationParams1): MQP {
|
|
86
|
+
const {
|
|
87
|
+
departmentId,
|
|
88
|
+
movedAt,
|
|
89
|
+
showAll,
|
|
90
|
+
} = params;
|
|
91
|
+
|
|
92
|
+
const query: FilterQuery<Location> = {
|
|
93
|
+
// TODO: Replace models.Location active: true with visibility in "visible"
|
|
94
|
+
active: true,
|
|
95
|
+
departmentId,
|
|
96
|
+
$and: excludePersonAndTransponderRecords,
|
|
97
|
+
kindType: {
|
|
98
|
+
$ne: "uav",
|
|
99
|
+
},
|
|
100
|
+
// 03/14/23 (Joe) - Trying movedAt only [sc-11794] - determined modified was being overwritten by cad vehicle status updates
|
|
101
|
+
movedAt: {
|
|
102
|
+
$gte: movedAt,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
if (showAll) {
|
|
106
|
+
delete query.active;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
list: query,
|
|
111
|
+
projection: projectionAdmin,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function adminListPersonLocations(params: AdminListLocationParams1): MQP {
|
|
116
|
+
const {
|
|
117
|
+
departmentId,
|
|
118
|
+
movedAt,
|
|
119
|
+
showAll
|
|
120
|
+
} = params;
|
|
121
|
+
|
|
122
|
+
const query: FilterQuery<Location> = {
|
|
123
|
+
active: true,
|
|
124
|
+
departmentId,
|
|
125
|
+
kindType: "person",
|
|
126
|
+
movedAt: {
|
|
127
|
+
$gte: movedAt,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
if (showAll) {
|
|
131
|
+
delete query.active;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Same fields defined in the frontend
|
|
135
|
+
// api/a1/location/person
|
|
136
|
+
const projection = {
|
|
137
|
+
_id: 1,
|
|
138
|
+
active: 1,
|
|
139
|
+
altitude: 1,
|
|
140
|
+
color: 1,
|
|
141
|
+
device_type: 1,
|
|
142
|
+
kindType: 1,
|
|
143
|
+
location: 1,
|
|
144
|
+
locationGeoJSON: 1,
|
|
145
|
+
modified: 1,
|
|
146
|
+
movedAt: 1,
|
|
147
|
+
personnelUUID: 1,
|
|
148
|
+
sendToCAD: 1,
|
|
149
|
+
source: 1,
|
|
150
|
+
speed: 1,
|
|
151
|
+
transponderUUID: 1,
|
|
152
|
+
uuid: 1, // Legacy, remove later
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
list: query,
|
|
157
|
+
projection,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function adminListUAVLocations(params: AdminListLocationParams1): MQP {
|
|
162
|
+
const {
|
|
163
|
+
departmentId,
|
|
164
|
+
movedAt,
|
|
165
|
+
showAll,
|
|
166
|
+
} = params;
|
|
167
|
+
|
|
168
|
+
const query: FilterQuery<Location> = {
|
|
169
|
+
active: true,
|
|
170
|
+
departmentId,
|
|
171
|
+
kindType: "uav",
|
|
172
|
+
movedAt: {
|
|
173
|
+
$gte: movedAt,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
if (showAll) {
|
|
177
|
+
delete query.active;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const projection = {
|
|
181
|
+
_id: 1,
|
|
182
|
+
active: 1,
|
|
183
|
+
agencyCode: 1,
|
|
184
|
+
agencyName: 1,
|
|
185
|
+
altitude: 1,
|
|
186
|
+
color: 1,
|
|
187
|
+
device_type: 1,
|
|
188
|
+
heading: 1,
|
|
189
|
+
kindType: 1,
|
|
190
|
+
locationGeoJSON: 1,
|
|
191
|
+
modified: 1,
|
|
192
|
+
movedAt: 1,
|
|
193
|
+
opAreaCode: 1,
|
|
194
|
+
opAreaName: 1,
|
|
195
|
+
sendToCAD: 1,
|
|
196
|
+
source: 1,
|
|
197
|
+
speed: 1,
|
|
198
|
+
state: 1,
|
|
199
|
+
userId: 1,
|
|
200
|
+
username: 1,
|
|
201
|
+
uuid: 1, // Legacy, remove later
|
|
202
|
+
vehicleId: 1,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
list: query,
|
|
207
|
+
projection,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function adminListAircraftLocations(params: AdminListLocationParams2): MQP {
|
|
212
|
+
const {
|
|
213
|
+
departmentId,
|
|
214
|
+
movedAt,
|
|
215
|
+
longitude,
|
|
216
|
+
latitude,
|
|
217
|
+
diagonal
|
|
218
|
+
} = params;
|
|
219
|
+
|
|
220
|
+
const list = {
|
|
221
|
+
shared: true,
|
|
222
|
+
active: true,
|
|
223
|
+
source: "ADSB",
|
|
224
|
+
kindType: {
|
|
225
|
+
$in: ["fixed-wing", "helicopter"],
|
|
226
|
+
},
|
|
227
|
+
departmentId: {
|
|
228
|
+
$ne: departmentId,
|
|
229
|
+
},
|
|
230
|
+
movedAt: {
|
|
231
|
+
$gte: movedAt.toISOString(),
|
|
232
|
+
},
|
|
233
|
+
locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
list,
|
|
238
|
+
projection: projectionAdmin,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function adminListSharedLocations(params: AdminListLocationParams2): MQP {
|
|
243
|
+
const {
|
|
244
|
+
departmentId,
|
|
245
|
+
movedAt,
|
|
246
|
+
longitude,
|
|
247
|
+
latitude,
|
|
248
|
+
diagonal
|
|
249
|
+
} = params;
|
|
250
|
+
|
|
251
|
+
const listSharedAVLQuery = {
|
|
252
|
+
shared: true,
|
|
253
|
+
active: true,
|
|
254
|
+
source: {
|
|
255
|
+
$ne: "ADSB"
|
|
256
|
+
},
|
|
257
|
+
departmentId: {
|
|
258
|
+
$ne: departmentId,
|
|
259
|
+
},
|
|
260
|
+
kindType: {
|
|
261
|
+
$ne: "uav",
|
|
262
|
+
},
|
|
263
|
+
movedAt: {
|
|
264
|
+
$gte: movedAt.toISOString(),
|
|
265
|
+
},
|
|
266
|
+
locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
list: listSharedAVLQuery,
|
|
271
|
+
projection: projectionAdmin,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function adminListFocusedLocations(params: AdminListLocationParams2): MQP {
|
|
276
|
+
const {
|
|
277
|
+
departmentId,
|
|
278
|
+
movedAt,
|
|
279
|
+
longitude,
|
|
280
|
+
latitude,
|
|
281
|
+
diagonal,
|
|
282
|
+
showAll
|
|
283
|
+
} = params;
|
|
284
|
+
|
|
285
|
+
const listFocusedAVLQuery: FilterQuery<Location> = {
|
|
286
|
+
active: true,
|
|
287
|
+
departmentId,
|
|
288
|
+
movedAt: {
|
|
289
|
+
$gte: movedAt.toISOString(),
|
|
290
|
+
},
|
|
291
|
+
locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (showAll) {
|
|
295
|
+
delete listFocusedAVLQuery.locationGeoJSON;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
list: listFocusedAVLQuery,
|
|
300
|
+
projection: projectionAdmin,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Sync queries
|
|
305
|
+
|
|
306
|
+
const projectionSync: ProjectionType<Location> = {
|
|
307
|
+
active: 1,
|
|
308
|
+
departmentId: 1,
|
|
309
|
+
device_type: 1, // used in web ui
|
|
310
|
+
location: 1,
|
|
311
|
+
locationGeoJSON: 1,
|
|
312
|
+
modified: 1,
|
|
313
|
+
userId: 1,
|
|
314
|
+
username: 1,
|
|
315
|
+
uuid: 1, // used in iOS
|
|
316
|
+
// both used by AVL sharing
|
|
317
|
+
opAreaName: 1,
|
|
318
|
+
opAreaCode: 1,
|
|
319
|
+
shared: 1,
|
|
320
|
+
state: 1,
|
|
321
|
+
// v3
|
|
322
|
+
altitude: 1,
|
|
323
|
+
speed: 1,
|
|
324
|
+
movedAt: 1,
|
|
325
|
+
agencyName: 1,
|
|
326
|
+
agencyCode: 1,
|
|
327
|
+
// v4
|
|
328
|
+
color: 1,
|
|
329
|
+
colorChangedAt: 1,
|
|
330
|
+
propsChangedAt: 1,
|
|
331
|
+
heading: 1,
|
|
332
|
+
// v4 extra
|
|
333
|
+
kindType: 1,
|
|
334
|
+
source: 1,
|
|
335
|
+
personnelUUID: 1,
|
|
336
|
+
transponderUUID: 1,
|
|
337
|
+
// v4 kind/type/locationType
|
|
338
|
+
kind: 1,
|
|
339
|
+
type: 1,
|
|
340
|
+
locationType: 1,
|
|
341
|
+
kindColor: 1,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export function syncListSharedLocations(params: AdminListLocationParams2 & SyncModifiedDate): MQP {
|
|
345
|
+
const {
|
|
346
|
+
departmentId,
|
|
347
|
+
modifiedDate,
|
|
348
|
+
movedAt,
|
|
349
|
+
longitude,
|
|
350
|
+
latitude,
|
|
351
|
+
diagonal,
|
|
352
|
+
} = params;
|
|
353
|
+
|
|
354
|
+
const list: FilterQuery<Location> = {
|
|
355
|
+
shared: true,
|
|
356
|
+
source: {
|
|
357
|
+
$nin: ["ADSB", "DroneSense"]
|
|
358
|
+
},
|
|
359
|
+
departmentId: {
|
|
360
|
+
$ne: departmentId,
|
|
361
|
+
},
|
|
362
|
+
modified: {
|
|
363
|
+
$gte: modifiedDate.toISOString(),
|
|
364
|
+
},
|
|
365
|
+
movedAt: {
|
|
366
|
+
$gte: movedAt.toISOString(),
|
|
367
|
+
},
|
|
368
|
+
locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
list,
|
|
373
|
+
projection: projectionSync,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function syncListLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
|
|
378
|
+
const {
|
|
379
|
+
departmentId,
|
|
380
|
+
modifiedDate,
|
|
381
|
+
movedAt,
|
|
382
|
+
} = params;
|
|
383
|
+
|
|
384
|
+
const list: FilterQuery<Location> = {
|
|
385
|
+
departmentId,
|
|
386
|
+
$and: excludePersonAndTransponderRecords,
|
|
387
|
+
kindType: {
|
|
388
|
+
$ne: "uav"
|
|
389
|
+
},
|
|
390
|
+
modified: {
|
|
391
|
+
$gte: modifiedDate.toISOString(),
|
|
392
|
+
},
|
|
393
|
+
movedAt: {
|
|
394
|
+
$gte: movedAt.toISOString(),
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
list,
|
|
400
|
+
projection: projectionSync,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function syncListPersonLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
|
|
405
|
+
const {
|
|
406
|
+
departmentId,
|
|
407
|
+
modifiedDate,
|
|
408
|
+
movedAt,
|
|
409
|
+
} = params;
|
|
410
|
+
|
|
411
|
+
const list: FilterQuery<Location> = {
|
|
412
|
+
departmentId,
|
|
413
|
+
kindType: "person",
|
|
414
|
+
modified: {
|
|
415
|
+
$gte: modifiedDate.toISOString(),
|
|
416
|
+
},
|
|
417
|
+
movedAt: {
|
|
418
|
+
$gte: movedAt.toISOString(),
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
list,
|
|
424
|
+
projection: projectionSync,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function syncListAircraftLocations(params: AdminListLocationParams2 & SyncModifiedDate): MQP {
|
|
429
|
+
const {
|
|
430
|
+
departmentId,
|
|
431
|
+
movedAt,
|
|
432
|
+
modifiedDate,
|
|
433
|
+
// Possibly missing
|
|
434
|
+
longitude,
|
|
435
|
+
latitude,
|
|
436
|
+
diagonal
|
|
437
|
+
} = params;
|
|
438
|
+
|
|
439
|
+
const list: FilterQuery<Location> = {
|
|
440
|
+
shared: true,
|
|
441
|
+
source: "ADSB",
|
|
442
|
+
kindType: {
|
|
443
|
+
$in: ["fixed-wing", "helicopter"]
|
|
444
|
+
},
|
|
445
|
+
departmentId: {
|
|
446
|
+
$ne: departmentId,
|
|
447
|
+
},
|
|
448
|
+
modified: {
|
|
449
|
+
$gte: modifiedDate.toISOString(),
|
|
450
|
+
},
|
|
451
|
+
movedAt: {
|
|
452
|
+
$gte: movedAt.toISOString(),
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const hasCoordinate = Math.abs(latitude) > 0.01 || Math.abs(longitude) > 0.01;
|
|
457
|
+
const validCoordinate = Math.abs(latitude) < 90 && Math.abs(longitude) < 180;
|
|
458
|
+
const filterOnLocation = hasCoordinate && validCoordinate;
|
|
459
|
+
if (filterOnLocation) {
|
|
460
|
+
list.locationGeoJSON = centerSphereFilter(longitude, latitude, diagonal);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
list,
|
|
465
|
+
projection: projectionSync,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function syncListUAVLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
|
|
470
|
+
const {
|
|
471
|
+
departmentId,
|
|
472
|
+
modifiedDate,
|
|
473
|
+
movedAt,
|
|
474
|
+
|
|
475
|
+
} = params;
|
|
476
|
+
|
|
477
|
+
const list: FilterQuery<Location> = {
|
|
478
|
+
departmentId,
|
|
479
|
+
kindType: "uav",
|
|
480
|
+
modified: {
|
|
481
|
+
$gte: modifiedDate.toISOString(),
|
|
482
|
+
},
|
|
483
|
+
movedAt: {
|
|
484
|
+
$gte: movedAt.toISOString(),
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
list,
|
|
490
|
+
projection: projectionSync,
|
|
491
|
+
};
|
|
492
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -2,6 +2,7 @@ import debugModule from "debug";
|
|
|
2
2
|
import {
|
|
3
3
|
DepartmentModel,
|
|
4
4
|
Location,
|
|
5
|
+
LocationKindType,
|
|
5
6
|
LocationModel,
|
|
6
7
|
CADVehicleModel,
|
|
7
8
|
CADVehicle,
|
|
@@ -48,14 +49,43 @@ export function store(
|
|
|
48
49
|
return await cadVehicleModel.findOneAndUpdate(query, item, options);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function isDuplicateKeyError(error: unknown): boolean {
|
|
53
|
+
return _.isObject(error) && (error as { code?: number }).code === 11000;
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
async function updateLocation(item: Partial<Location>, atDate: Date) {
|
|
57
|
+
try {
|
|
58
|
+
await performUpdateLocation(item, atDate);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!isDuplicateKeyError(error)) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
// A concurrent first-write hit the unique (departmentId, session, device_type)
|
|
64
|
+
// index; the record exists now so we can retry once, so this update goes through.
|
|
65
|
+
await performUpdateLocation(item, atDate);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function performUpdateLocation(item: Partial<Location>, atDate: Date) {
|
|
52
70
|
// eslint-disable-next-line new-cap
|
|
53
71
|
const modelItem = new locationModel(item);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
const hasPersonIdentity = (_.isString(modelItem.personnelUUID) && modelItem.personnelUUID !== "") ||
|
|
73
|
+
(_.isString(modelItem.transponderUUID) && modelItem.transponderUUID !== "");
|
|
74
|
+
const isPerson = (modelItem.kindType === LocationKindType.Person || modelItem.locationType === LocationKindType.Person) &&
|
|
75
|
+
hasPersonIdentity;
|
|
76
|
+
// Person records upsert by session (their stable identity key); username is
|
|
77
|
+
// display-only for transponder stuff, so renames must not create new records from updates
|
|
78
|
+
const query = isPerson
|
|
79
|
+
? {
|
|
80
|
+
device_type: modelItem.device_type,
|
|
81
|
+
departmentId: modelItem.departmentId,
|
|
82
|
+
session: modelItem.session
|
|
83
|
+
}
|
|
84
|
+
: {
|
|
85
|
+
device_type: modelItem.device_type,
|
|
86
|
+
departmentId: modelItem.departmentId,
|
|
87
|
+
username: modelItem.username
|
|
88
|
+
};
|
|
59
89
|
debug(`Location.findOne: ${JSON.stringify(query)}`);
|
|
60
90
|
const dbItem = await locationModel.findOne(query).lean().exec() as Partial<LocationObject>;
|
|
61
91
|
const cloneItem = { ...dbItem };
|
|
@@ -95,6 +125,25 @@ export function store(
|
|
|
95
125
|
dbItem.locationGeoJSON = modelItem.locationGeoJSON;
|
|
96
126
|
dbItem.source = modelItem.source;
|
|
97
127
|
dbItem.kindType = modelItem.kindType;
|
|
128
|
+
dbItem.industry = modelItem.industry;
|
|
129
|
+
// Carry person/category fields on the update path; only overwrite when the
|
|
130
|
+
// incoming payload actually provides a value, so sources that don't send
|
|
131
|
+
// these fields don't wipe values written by other sources.
|
|
132
|
+
if (_.isString(modelItem.locationType) && modelItem.locationType !== "") {
|
|
133
|
+
dbItem.locationType = modelItem.locationType;
|
|
134
|
+
}
|
|
135
|
+
if (_.isString(modelItem.kind) && modelItem.kind !== "") {
|
|
136
|
+
dbItem.kind = modelItem.kind;
|
|
137
|
+
}
|
|
138
|
+
if (_.isString(modelItem.type) && modelItem.type !== "") {
|
|
139
|
+
dbItem.type = modelItem.type;
|
|
140
|
+
}
|
|
141
|
+
if (_.isString(modelItem.personnelUUID) && modelItem.personnelUUID !== "") {
|
|
142
|
+
dbItem.personnelUUID = modelItem.personnelUUID;
|
|
143
|
+
}
|
|
144
|
+
if (_.isString(modelItem.transponderUUID) && modelItem.transponderUUID !== "") {
|
|
145
|
+
dbItem.transponderUUID = modelItem.transponderUUID;
|
|
146
|
+
}
|
|
98
147
|
|
|
99
148
|
// Propagate sharedAVL items
|
|
100
149
|
dbItem.opAreaCode = modelItem.opAreaCode;
|
|
@@ -135,6 +184,7 @@ export function store(
|
|
|
135
184
|
"shared",
|
|
136
185
|
"state",
|
|
137
186
|
"username",
|
|
187
|
+
"industry",
|
|
138
188
|
];
|
|
139
189
|
if (_.intersection(updatedKeys, propsKeys).length > 0) {
|
|
140
190
|
locationDelta.propsChangedAt = atDate;
|