@tak-ps/node-cot 13.5.0 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,401 @@
1
+ import Err from '@openaddresses/batch-error';
2
+ import { v4 as randomUUID } from 'uuid';
3
+ import type { Static } from '@sinclair/typebox';
4
+ import type {
5
+ LinkAttributes,
6
+ } from '../types/types.js'
7
+ import {
8
+ InputFeature,
9
+ } from '../types/feature.js';
10
+ import type { AllGeoJSON } from "@turf/helpers";
11
+ import TypeValidator from '../type.js';
12
+ import PointOnFeature from '@turf/point-on-feature';
13
+ import Util from '../utils/util.js';
14
+ import Color from '../utils/color.js';
15
+ import JSONCoT from '../types/types.js'
16
+ import CoT from '../cot.js';
17
+ import type { CoTOptions } from '../cot.js';
18
+
19
+ /**
20
+ * Return an CoT Message given a GeoJSON Feature
21
+ *
22
+ * @param {Object} feature GeoJSON Point Feature
23
+ *
24
+ * @return {CoT}
25
+ */
26
+ export async function from_geojson(
27
+ feature: Static<typeof InputFeature>,
28
+ opts: CoTOptions = {}
29
+ ): Promise<CoT> {
30
+ try {
31
+ feature = await TypeValidator.type(InputFeature, feature);
32
+ } catch (err) {
33
+ throw new Err(400, null, `Validation Error: ${err}`);
34
+ }
35
+
36
+ const cot: Static<typeof JSONCoT> = {
37
+ event: {
38
+ _attributes: Util.cot_event_attr(
39
+ feature.properties.type || 'a-f-G',
40
+ feature.properties.how || 'm-g',
41
+ feature.properties.time,
42
+ feature.properties.start,
43
+ feature.properties.stale
44
+ ),
45
+ point: Util.cot_point(),
46
+ detail: Util.cot_event_detail(feature.properties.callsign)
47
+ }
48
+ };
49
+
50
+ if (feature.id) {
51
+ cot.event._attributes.uid = String(feature.id);
52
+ } else {
53
+ cot.event._attributes.uid = randomUUID();
54
+ }
55
+
56
+ if (!cot.event.detail) cot.event.detail = {};
57
+
58
+ if (feature.properties.droid) {
59
+ cot.event.detail.uid = { _attributes: { Droid: feature.properties.droid } };
60
+ }
61
+
62
+ if (feature.properties.archived) {
63
+ cot.event.detail.archive = { _attributes: { } };
64
+ }
65
+
66
+ if (feature.properties.links) {
67
+ if (!cot.event.detail.link) cot.event.detail.link = [];
68
+ else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link];
69
+
70
+ cot.event.detail.link.push(...feature.properties.links.map((link: Static<typeof LinkAttributes>) => {
71
+ return { _attributes: link };
72
+ }))
73
+ }
74
+
75
+ if (feature.properties.dest) {
76
+ const dest = !Array.isArray(feature.properties.dest) ? [ feature.properties.dest ] : feature.properties.dest;
77
+
78
+ cot.event.detail.marti = {
79
+ dest: dest.map((dest) => {
80
+ return { _attributes: { ...dest } };
81
+ })
82
+ }
83
+ }
84
+
85
+ if (feature.properties.type === 'b-a-o-tbl') {
86
+ cot.event.detail.emergency = {
87
+ _attributes: { type: '911 Alert' },
88
+ _text: feature.properties.callsign || 'UNKNOWN'
89
+ }
90
+ } else if (feature.properties.type === 'b-a-o-can') {
91
+ cot.event.detail.emergency = {
92
+ _attributes: { cancel: true },
93
+ _text: feature.properties.callsign || 'UNKNOWN'
94
+ }
95
+ } else if (feature.properties.type === 'b-a-g') {
96
+ cot.event.detail.emergency = {
97
+ _attributes: { type: 'Geo-fence Breached' },
98
+ _text: feature.properties.callsign || 'UNKNOWN'
99
+ }
100
+ } else if (feature.properties.type === 'b-a-o-pan') {
101
+ cot.event.detail.emergency = {
102
+ _attributes: { type: 'Ring The Bell' },
103
+ _text: feature.properties.callsign || 'UNKNOWN'
104
+ }
105
+ } else if (feature.properties.type === 'b-a-o-opn') {
106
+ cot.event.detail.emergency = {
107
+ _attributes: { type: 'Troops In Contact' },
108
+ _text: feature.properties.callsign || 'UNKNOWN'
109
+ }
110
+ }
111
+
112
+ if (feature.properties.takv) {
113
+ cot.event.detail.takv = { _attributes: { ...feature.properties.takv } };
114
+ }
115
+
116
+ if (feature.properties.creator) {
117
+ cot.event.detail.creator = { _attributes: { ...feature.properties.creator } };
118
+ }
119
+
120
+ if (feature.properties.range !== undefined) {
121
+ cot.event.detail.range = { _attributes: { value: feature.properties.range } }
122
+ }
123
+
124
+ if (feature.properties.bearing !== undefined) {
125
+ cot.event.detail.bearing = { _attributes: { value: feature.properties.bearing } }
126
+ }
127
+
128
+ if (feature.properties.geofence) {
129
+ cot.event.detail.__geofence = { _attributes: { ...feature.properties.geofence } };
130
+ }
131
+
132
+ if (feature.properties.milsym) {
133
+ cot.event.detail.__milsym = { _attributes: { id: feature.properties.milsym.id} };
134
+ }
135
+
136
+ if (feature.properties.sensor) {
137
+ cot.event.detail.sensor = { _attributes: { ...feature.properties.sensor } };
138
+ }
139
+
140
+ if (feature.properties.ackrequest) {
141
+ cot.event.detail.ackrequest = { _attributes: { ...feature.properties.ackrequest } };
142
+ }
143
+
144
+ if (feature.properties.video) {
145
+ if (feature.properties.video.connection) {
146
+ const video = JSON.parse(JSON.stringify(feature.properties.video));
147
+
148
+ const connection = video.connection;
149
+ delete video.connection;
150
+
151
+ cot.event.detail.__video = {
152
+ _attributes: { ...video },
153
+ ConnectionEntry: {
154
+ _attributes: connection
155
+ }
156
+ }
157
+ } else {
158
+ cot.event.detail.__video = { _attributes: { ...feature.properties.video } };
159
+ }
160
+ }
161
+
162
+ if (feature.properties.attachments) {
163
+ cot.event.detail.attachment_list = { _attributes: { hashes: JSON.stringify(feature.properties.attachments) } };
164
+ }
165
+
166
+ if (feature.properties.contact) {
167
+ cot.event.detail.contact = {
168
+ _attributes: {
169
+ ...feature.properties.contact,
170
+ callsign: feature.properties.callsign || 'UNKNOWN',
171
+ }
172
+ };
173
+ }
174
+
175
+ if (feature.properties.fileshare) {
176
+ cot.event.detail.fileshare = { _attributes: { ...feature.properties.fileshare } };
177
+ }
178
+
179
+ if (feature.properties.course !== undefined || feature.properties.speed !== undefined || feature.properties.slope !== undefined) {
180
+ cot.event.detail.track = {
181
+ _attributes: Util.cot_track_attr(feature.properties.course, feature.properties.speed, feature.properties.slope)
182
+ }
183
+ }
184
+
185
+ if (feature.properties.group) {
186
+ cot.event.detail.__group = { _attributes: { ...feature.properties.group } }
187
+ }
188
+
189
+ if (feature.properties.flow) {
190
+ cot.event.detail['_flow-tags_'] = { _attributes: { ...feature.properties.flow } }
191
+ }
192
+
193
+ if (feature.properties.status) {
194
+ cot.event.detail.status = { _attributes: { ...feature.properties.status } }
195
+ }
196
+
197
+ if (feature.properties.precisionlocation) {
198
+ cot.event.detail.precisionlocation = { _attributes: { ...feature.properties.precisionlocation } }
199
+ }
200
+
201
+ if (feature.properties.icon) {
202
+ cot.event.detail.usericon = { _attributes: { iconsetpath: feature.properties.icon } }
203
+ }
204
+
205
+ if (feature.properties.mission) {
206
+ cot.event.detail.mission = {
207
+ _attributes: {
208
+ type: feature.properties.mission.type,
209
+ guid: feature.properties.mission.guid,
210
+ tool: feature.properties.mission.tool,
211
+ name: feature.properties.mission.name,
212
+ authorUid: feature.properties.mission.authorUid,
213
+ }
214
+ }
215
+
216
+ if (feature.properties.mission.missionLayer) {
217
+ cot.event.detail.mission.missionLayer = {};
218
+
219
+ if (feature.properties.mission.missionLayer.name) {
220
+ cot.event.detail.mission.missionLayer.name = { _text: feature.properties.mission.missionLayer.name };
221
+ }
222
+
223
+ if (feature.properties.mission.missionLayer.parentUid) {
224
+ cot.event.detail.mission.missionLayer.parentUid = { _text: feature.properties.mission.missionLayer.parentUid };
225
+ }
226
+
227
+ if (feature.properties.mission.missionLayer.type) {
228
+ cot.event.detail.mission.missionLayer.type = { _text: feature.properties.mission.missionLayer.type };
229
+ }
230
+
231
+ if (feature.properties.mission.missionLayer.uid) {
232
+ cot.event.detail.mission.missionLayer.uid = { _text: feature.properties.mission.missionLayer.uid };
233
+ }
234
+ }
235
+ }
236
+
237
+ cot.event.detail.remarks = { _attributes: { }, _text: feature.properties.remarks || '' };
238
+
239
+ if (!feature.geometry) {
240
+ throw new Err(400, null, 'Must have Geometry');
241
+ } else if (!['Point', 'Polygon', 'LineString'].includes(feature.geometry.type)) {
242
+ throw new Err(400, null, 'Unsupported Geometry Type');
243
+ }
244
+
245
+ // This isn't specific to point as the color can apply to the centroid point
246
+ if (feature.properties['marker-color']) {
247
+ const color = new Color(feature.properties['marker-color'] || -1761607936);
248
+ color.a = feature.properties['marker-opacity'] !== undefined ? feature.properties['marker-opacity'] * 255 : 128;
249
+
250
+ cot.event.detail.color = {
251
+ _attributes: {
252
+ argb: color.as_32bit(),
253
+ value: color.as_32bit()
254
+ }
255
+ };
256
+ }
257
+
258
+ if (feature.geometry.type === 'Point') {
259
+ cot.event.point._attributes.lon = feature.geometry.coordinates[0];
260
+ cot.event.point._attributes.lat = feature.geometry.coordinates[1];
261
+ cot.event.point._attributes.hae = feature.geometry.coordinates[2] || 0.0;
262
+ } else if (['Polygon', 'LineString'].includes(feature.geometry.type)) {
263
+ const stroke = new Color(feature.properties.stroke || -1761607936);
264
+ stroke.a = feature.properties['stroke-opacity'] !== undefined ? feature.properties['stroke-opacity'] * 255 : 128;
265
+ cot.event.detail.strokeColor = { _attributes: { value: stroke.as_32bit() } };
266
+
267
+ if (!feature.properties['stroke-width']) feature.properties['stroke-width'] = 3;
268
+ cot.event.detail.strokeWeight = { _attributes: {
269
+ value: feature.properties['stroke-width']
270
+ } };
271
+
272
+ if (!feature.properties['stroke-style']) feature.properties['stroke-style'] = 'solid';
273
+ cot.event.detail.strokeStyle = { _attributes: {
274
+ value: feature.properties['stroke-style']
275
+ } };
276
+
277
+
278
+ if (feature.geometry.type === 'Polygon' && feature.properties.type && ['u-d-c-c', 'u-r-b-c-c'].includes(feature.properties.type)) {
279
+ if (!feature.properties.shape || !feature.properties.shape.ellipse) {
280
+ throw new Err(400, null, `${feature.properties.type} (Circle) must define a feature.properties.shape.ellipse property`)
281
+ }
282
+
283
+ const strokeColor = (cot.event.detail.strokeColor?._attributes?.value) ? new Color(cot.event.detail.strokeColor._attributes.value) : new Color('#00FF0000');
284
+ const fillColor = (cot.event.detail.fillColor?._attributes?.value) ? new Color(cot.event.detail.fillColor._attributes.value) : new Color('#00FF0000');
285
+
286
+ cot.event.detail.shape = {
287
+ ellipse: {
288
+ _attributes: feature.properties.shape.ellipse
289
+ },
290
+ link: {
291
+ _attributes: {
292
+ uid: `${cot.event._attributes.uid}.Style`,
293
+ type: 'b-x-KmlStyle',
294
+ relation: 'p-c'
295
+ },
296
+ Style: {
297
+ LineStyle: {
298
+ color: { _text: strokeColor.as_hexa().slice(1) },
299
+ width: { _text: cot.event.detail.strokeWeight?._attributes?.value ? cot.event.detail.strokeWeight._attributes.value : 3 }
300
+ },
301
+ PolyStyle: {
302
+ color: { _text: fillColor.as_hexa().slice(1) },
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ } else if (feature.geometry.type === 'LineString' && feature.properties.type === 'b-m-r') {
309
+ cot.event._attributes.type = 'b-m-r';
310
+
311
+ if (!cot.event.detail.link) {
312
+ cot.event.detail.link = [];
313
+ } else if (!Array.isArray(cot.event.detail.link)) {
314
+ cot.event.detail.link = [cot.event.detail.link]
315
+ }
316
+
317
+ cot.event.detail.__routeinfo = {
318
+ __navcues: {
319
+ __navcue: []
320
+ }
321
+ }
322
+
323
+ for (const coord of feature.geometry.coordinates) {
324
+ cot.event.detail.link.push({
325
+ _attributes: {
326
+ type: 'b-m-p-c',
327
+ uid: randomUUID(),
328
+ callsign: "",
329
+ point: `${coord[1]},${coord[0]}`
330
+ }
331
+ });
332
+ }
333
+ } else if (feature.geometry.type === 'LineString') {
334
+ cot.event._attributes.type = 'u-d-f';
335
+
336
+ if (!cot.event.detail.link) {
337
+ cot.event.detail.link = [];
338
+ } else if (!Array.isArray(cot.event.detail.link)) {
339
+ cot.event.detail.link = [cot.event.detail.link]
340
+ }
341
+
342
+ for (const coord of feature.geometry.coordinates) {
343
+ cot.event.detail.link.push({
344
+ _attributes: { point: `${coord[1]},${coord[0]}` }
345
+ });
346
+ }
347
+ } else if (feature.geometry.type === 'Polygon') {
348
+ cot.event._attributes.type = 'u-d-f';
349
+
350
+ if (!cot.event.detail.link) cot.event.detail.link = [];
351
+ else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link]
352
+
353
+ // Inner rings are not yet supported
354
+ for (const coord of feature.geometry.coordinates[0]) {
355
+ cot.event.detail.link.push({
356
+ _attributes: { point: `${coord[1]},${coord[0]}` }
357
+ });
358
+ }
359
+
360
+ const fill = new Color(feature.properties.fill || -1761607936);
361
+ fill.a = feature.properties['fill-opacity'] !== undefined ? feature.properties['fill-opacity'] * 255 : 128;
362
+ cot.event.detail.fillColor = { _attributes: { value: fill.as_32bit() } };
363
+ }
364
+
365
+ if (feature.properties.labels) {
366
+ cot.event.detail.labels_on = { _attributes: { value: feature.properties.labels } };
367
+ } else {
368
+ cot.event.detail.labels_on = { _attributes: { value: false } };
369
+ }
370
+
371
+ cot.event.detail.tog = { _attributes: { enabled: '0' } };
372
+
373
+ if (feature.properties.center && Array.isArray(feature.properties.center) && feature.properties.center.length >= 2) {
374
+ cot.event.point._attributes.lon = feature.properties.center[0];
375
+ cot.event.point._attributes.lat = feature.properties.center[1];
376
+
377
+ if (feature.properties.center.length >= 3) {
378
+ cot.event.point._attributes.hae = feature.properties.center[2] || 0.0;
379
+ } else {
380
+ cot.event.point._attributes.hae = 0.0;
381
+ }
382
+ } else {
383
+ const centre = PointOnFeature(feature as AllGeoJSON);
384
+ cot.event.point._attributes.lon = centre.geometry.coordinates[0];
385
+ cot.event.point._attributes.lat = centre.geometry.coordinates[1];
386
+ cot.event.point._attributes.hae = 0.0;
387
+ }
388
+ }
389
+
390
+ const newcot = new CoT(cot, opts);
391
+
392
+ if (feature.properties.metadata) {
393
+ newcot.metadata = feature.properties.metadata
394
+ }
395
+
396
+ if (feature.path) {
397
+ newcot.path = feature.path
398
+ }
399
+
400
+ return newcot;
401
+ }
@@ -0,0 +1,104 @@
1
+ import Err from '@openaddresses/batch-error';
2
+ import { v4 as randomUUID } from 'uuid';
3
+ import type { Static } from '@sinclair/typebox';
4
+ import { coordEach } from "@turf/meta";
5
+ import TypeValidator from '../type.js';
6
+ import { Feature } from '../types/feature.js';
7
+ import PointOnFeature from '@turf/point-on-feature';
8
+ import { GeoJSONFeature } from '../types/geojson.js';
9
+
10
+ /**
11
+ * Given a generic GeoJSON Feature, convert it to a CoT Featurt
12
+ *
13
+ * @param {Object} feature GeoJSON Feature
14
+ *
15
+ * @return {CoT}
16
+ */
17
+ export async function normalize_geojson(
18
+ feature: Static<typeof GeoJSONFeature>,
19
+ ): Promise<Static<typeof Feature>> {
20
+ try {
21
+ feature = await TypeValidator.type(GeoJSONFeature, feature);
22
+ } catch (err) {
23
+ throw new Err(400, null, `Validation Error: ${err}`);
24
+ }
25
+
26
+ if (!feature.id) {
27
+ feature.id = randomUUID();
28
+ }
29
+
30
+ const props = feature.properties;
31
+
32
+ feature.properties = {
33
+ metadata: props || {}
34
+ }
35
+
36
+ if (feature.geometry.type === 'Point') {
37
+ feature.properties.type = 'u-d-p';
38
+ } else if (feature.geometry.type === 'LineString') {
39
+ feature.properties.type = 'u-d-f';
40
+ } else if (feature.geometry.type === 'Polygon') {
41
+ feature.properties.type = 'u-d-f';
42
+ } else {
43
+ throw new Err(400, null, `Unsupported Geometry Type`);
44
+ }
45
+
46
+ for (const color of ['marker-color', 'stroke', 'fill']) {
47
+ if (
48
+ props[color]
49
+ && typeof props[color] === 'string'
50
+ ) {
51
+ feature.properties[color] = props[color];
52
+ }
53
+ }
54
+
55
+ for (const number of ['marker-opacity', 'stroke-opacity', 'stroke-width', 'fill-opacity']) {
56
+ if (
57
+ props[number]
58
+ && typeof props[number] === 'number'
59
+ ) {
60
+ feature.properties[number] = props[number];
61
+ }
62
+ }
63
+
64
+ // Callsign Options
65
+ for (const callsign of ['callsign', 'title', 'name']) {
66
+ if (props[callsign] && typeof props[callsign] === 'string') {
67
+ feature.properties.callsign = props[callsign];
68
+ break;
69
+ }
70
+ }
71
+
72
+ if (!feature.properties.callsign) {
73
+ feature.properties.callsign = 'New Feature';
74
+ }
75
+
76
+ // Remarks Options
77
+ for (const remarks of ['remarks', 'description']) {
78
+ if (props[remarks] && typeof props[remarks] === 'string') {
79
+ feature.properties.remarks = props[remarks];
80
+ break;
81
+ }
82
+ }
83
+
84
+ if (!feature.properties.remarks) {
85
+ feature.properties.remarks = '';
86
+ }
87
+
88
+ feature.properties.time = new Date().toISOString();
89
+ feature.properties.start = new Date().toISOString();
90
+
91
+ const stale = new Date();
92
+ stale.setHours(stale.getHours() + 1);
93
+ feature.properties.stale = stale;
94
+
95
+ feature.properties.center = PointOnFeature(feature).geometry.coordinates;
96
+
97
+ feature.properties.archived = feature.properties.archived || false;
98
+
99
+ coordEach(feature.geometry, (coord) => {
100
+ return coord.slice(0, 3);
101
+ });
102
+
103
+ return feature as Static<typeof Feature>;
104
+ }