signalk-vessels-to-ais 1.1.1 → 1.1.5

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.
Files changed (4) hide show
  1. package/.eslintrc.js +24 -0
  2. package/README.md +5 -0
  3. package/index.js +338 -175
  4. package/package.json +10 -3
package/.eslintrc.js ADDED
@@ -0,0 +1,24 @@
1
+ module.exports = {
2
+ env: {
3
+ commonjs: true,
4
+ es2021: true,
5
+ node: true,
6
+ browser: true,
7
+ jquery: true,
8
+ },
9
+ extends: [
10
+ 'airbnb-base',
11
+ ],
12
+ parserOptions: {
13
+ ecmaVersion: 12,
14
+ },
15
+ rules: {
16
+ 'linebreak-style': 0,
17
+ 'no-console': 0,
18
+ 'func-names': 0,
19
+ 'prefer-destructuring': 0,
20
+ 'one-var-declaration-per-line': 0,
21
+ 'one-var': 0,
22
+ 'no-plusplus': 0,
23
+ },
24
+ };
package/README.md CHANGED
@@ -6,8 +6,13 @@ SignalK server plugin to convert other vessel data to NMEA0183 AIS format and fo
6
6
 
7
7
  User can configure:
8
8
  - How often data is sent out
9
+ - Own data can be added to AIS sending
9
10
 
10
11
  New:
12
+ - v1.1.5, updated vessels within selected timeframe are sent out, radius filtering around own vessel and tag-block option added
13
+ - v1.1.4, small fix
14
+ - v1.1.3, add: own vessel data and sending interval modified
15
+ - v1.1.2, fix: http/https url selection and better error info
11
16
  - v1.1.1, fix: current status of the plugin updated
12
17
  - v1.1.0, fix: numeric value test for text strings of AIS
13
18
  - v1.0.0, v1 release
package/index.js CHANGED
@@ -22,8 +22,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
22
  SOFTWARE.
23
23
  */
24
24
 
25
- const fetch = require('node-fetch');
26
- const AisEncode = require("ggencoder").AisEncode
25
+ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
26
+ const AisEncode = require('ggencoder').AisEncode;
27
+ const moment = require('moment');
28
+ const haversine = require('haversine-distance');
27
29
 
28
30
  module.exports = function createPlugin(app) {
29
31
  const plugin = {};
@@ -31,229 +33,365 @@ module.exports = function createPlugin(app) {
31
33
  plugin.name = 'Other vessels data to AIS NMEA0183';
32
34
  plugin.description = 'SignalK server plugin to convert other vessel data to NMEA0183 AIS format and forward it out to 3rd party applications';
33
35
 
34
- var position_update = null;
35
- var url;
36
- var interval_id1;
37
- var interval_id2;
38
- var unsubscribes = [];
36
+ let positionUpdate = null;
37
+ let distance;
38
+ let sendOwn;
39
+ let url;
40
+ let intervalRun;
41
+ let readInfo;
39
42
  const setStatus = app.setPluginStatus || app.setProviderStatus;
40
43
 
41
- plugin.start = function (options, restartPlugin) {
44
+ let position_update;
45
+ let useTag;
46
+
47
+ plugin.start = function (options, restartPlugin) {
48
+ position_update = options.position_update * 60;
49
+ useTag = options.useTag;
50
+
51
+ let port = options.port || 3000;
52
+ let portSec = options.portSec || 3443;
53
+
54
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
55
+ url = 'https://localhost:' + portSec + '/signalk/v1/api/vessels';
56
+ fetch(url, { method: 'GET' })
57
+ .then((res) => {
58
+ console.log(`${plugin.id}: SSL enabled, using https`);
59
+ if (!res.ok) {
60
+ console.error(`${plugin.id}: SSL enabled, but error accessing server. Check 'Allow Readonly Access' and enable it.`);
61
+ setStatus("Error accessing server. Check 'Allow Readonly Access' and enable it");
62
+ }
63
+ })
64
+ .catch(() => {
65
+ url = 'http://localhost:' + port + '/signalk/v1/api/vessels';
66
+ fetch(url, { method: 'GET' })
67
+ .then((res) => {
68
+ console.log(`${plugin.id}: SSL disabled, using http`);
69
+ if (!res.ok) {
70
+ console.error(`${plugin.id}: SSL disabled, but error accessing server. Check 'Allow Readonly Access' and enable it.`);
71
+ setStatus("Error accessing server. Check 'Allow Readonly Access' and enable it");
72
+ }
73
+ })
74
+ .catch(() => {
75
+ });
76
+ });
42
77
 
43
- position_update = options.position_update
44
- app.debug('Plugin started');
78
+ positionUpdate = options.position_update;
79
+ distance = options.distance;
80
+ sendOwn = options.sendOwn;
45
81
 
46
- interval_id1 = setInterval(read_info,(15000));
47
- setTimeout(clear, 15000);
48
- interval_id2 = setInterval(read_info,(position_update * 60000));
82
+ app.debug('Plugin started');
49
83
 
84
+ intervalRun = setInterval(readInfo, (positionUpdate * 60000));
50
85
  };
51
86
 
52
- //----------------------------------------------------------------------------
53
- // State Mapping
54
-
55
- let stateMapping = {
56
- 'motoring': 0,
57
- 'anchored': 1,
58
- 'not under command': 2,
59
- 'restricted manouverability': 3,
60
- 'constrained by draft': 4,
61
- 'moored': 5,
62
- 'aground': 6,
63
- 'fishing': 7,
64
- 'sailing': 8,
65
- 'hazardous material high speed': 9,
66
- 'hazardous material wing in ground': 10,
67
- 'ais-sart': 14,
68
- 'default': 15
69
- }
70
-
71
- //----------------------------------------------------------------------------
72
- // Rad to Deg
73
- function radians_to_degrees(radians)
74
- {
75
- var pi = Math.PI;
76
- return ((radians * 180)/pi);
77
- }
78
-
79
- //----------------------------------------------------------------------------
80
- // m/s to knots
81
- function ms_to_knots(speed)
82
- {
83
- return ((speed * 3.6) / 1.852);
84
- }
85
-
86
- //----------------------------------------------------------------------------
87
- // Clear start interval
88
-
89
- function clear() {
90
- clearInterval(interval_id1);
87
+ //----------------------------------------------------------------------------
88
+ // State Mapping
89
+
90
+ const stateMapping = {
91
+ motoring: 0,
92
+ anchored: 1,
93
+ 'not under command': 2,
94
+ 'restricted manouverability': 3,
95
+ 'constrained by draft': 4,
96
+ moored: 5,
97
+ aground: 6,
98
+ fishing: 7,
99
+ sailing: 8,
100
+ 'hazardous material high speed': 9,
101
+ 'hazardous material wing in ground': 10,
102
+ 'ais-sart': 14,
103
+ default: 15,
91
104
  };
92
105
 
93
- //----------------------------------------------------------------------------
94
- // json size
95
- function lengthInUtf8Bytes(str,str2) {
96
- var m = encodeURIComponent(str).match(/%[89ABab]/g);
97
- return (((str.length + (m ? m.length : 0))/1024) + (str2*0.2)).toFixed(1);
98
- }
99
-
100
- //----------------------------------------------------------------------------
101
- // nmea out
102
-
103
- function ais_out(enc_msg) {
104
- var enc= new AisEncode(enc_msg)
105
- var sentence = enc.nmea
106
- if ( sentence && sentence.length > 0 )
107
- {
108
- app.debug(sentence);
109
- app.emit('nmea0183out', sentence);
106
+ //----------------------------------------------------------------------------
107
+ // Rad to Deg
108
+ function radToDegrees(radians) {
109
+ const pi = Math.PI;
110
+ return ((radians * 180) / pi);
111
+ }
112
+
113
+ //----------------------------------------------------------------------------
114
+ // m/s to knots
115
+ function msToKnots(speed) {
116
+ return ((speed * 3.6) / 1.852);
110
117
  }
111
- }
112
118
 
113
- //----------------------------------------------------------------------------
114
- // Read and parse AIS data
119
+ //----------------------------------------------------------------------------
120
+ // nmea out
121
+
122
+ function aisOut(encMsg, aisTime) {
123
+ const enc = new AisEncode(encMsg);
124
+ const sentence = enc.nmea;
125
+ let taggString = '';
126
+ if (useTag) {
127
+ taggString = createTagBlock(aisTime)
128
+ }
129
+ if (sentence && sentence.length > 0) {
130
+ app.debug(taggString+sentence);
131
+ app.emit('nmea0183out', taggString+sentence);
132
+ }
133
+ }
115
134
 
116
- read_info = function read_data() {
117
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
118
- var url ="http://localhost:3000/signalk/v1/api/vessels";
135
+ const m_hex = [
136
+ '0',
137
+ '1',
138
+ '2',
139
+ '3',
140
+ '4',
141
+ '5',
142
+ '6',
143
+ '7',
144
+ '8',
145
+ '9',
146
+ 'A',
147
+ 'B',
148
+ 'C',
149
+ 'D',
150
+ 'E',
151
+ 'F'
152
+ ]
153
+
154
+ function toHexString (v) {
155
+ let msn = (v >> 4) & 0x0f
156
+ let lsn = (v >> 0) & 0x0f
157
+ return m_hex[msn] + m_hex[lsn]
158
+ }
119
159
 
120
- fetch(url, { method: 'GET'})
121
- .then((res) => {
122
- return res.json()
123
- })
124
- .then((json) => {
125
- var jsonContent = JSON.parse(JSON.stringify(json));
126
- var numberAIS = Object.keys(jsonContent).length;
127
- for (i = 1; i < numberAIS; i++) {
128
- var jsonKey = Object.keys(jsonContent)[i];
129
-
130
- try { var mmsi = jsonContent[jsonKey].mmsi;} catch (error) {mmsi = null;};
131
- try { var name = jsonContent[jsonKey].name;} catch (error) {name = "";};
132
- try { var lat = jsonContent[jsonKey].navigation.position.value.latitude;} catch (error) {lat = null ;};
133
- try { var lon = jsonContent[jsonKey].navigation.position.value.longitude;} catch (error) {lon = null;};
134
- try { var sog = ms_to_knots(jsonContent[jsonKey].navigation.speedOverGround.value);} catch (error) {sog = null;};
135
- try { var cog = radians_to_degrees(jsonContent[jsonKey].navigation.courseOverGroundTrue.value);} catch (error) {cog = null;};
136
- try { var rot = radians_to_degrees(jsonContent[jsonKey].navigation.rateOfTurn.value);} catch (error) {rot = null;};
137
- try { var navStat = stateMapping[jsonContent[jsonKey].navigation.state.value];} catch (error) {navStat = "";};
138
- try { var hdg = radians_to_degrees(jsonContent[jsonKey].navigation.headingTrue.value);} catch (error) {hdg = null;};
139
- try { var dst = jsonContent[jsonKey].navigation.destination.commonName.value;} catch (error) {dst = "";};
140
- try { var callSign = jsonContent[jsonKey].communication.callsignVhf;} catch (error) {callSign = "";};
141
- try { var imo = (jsonContent[jsonKey].registrations.imo).substring(4, 20);} catch (error) {imo = null;};
142
- try { var id = jsonContent[jsonKey].design.aisShipType.value.id;} catch (error) {id = null;};
143
- try { var type = jsonContent[jsonKey].design.aisShipType.value.name;} catch (error) {type = "";};
144
- try { var draft_cur = (jsonContent[jsonKey].design.draft.value.current)/10;} catch (error) {draft_cur = null;};
145
- try { var length = jsonContent[jsonKey].design.length.value.overall;} catch (error) {length = null;};
146
- try { var beam = (jsonContent[jsonKey].design.beam.value)/2;} catch (error) {beam = null;};
147
- try { var ais = jsonContent[jsonKey].sensors.ais.class.value;} catch (error) {ais = null;};
148
-
149
- if (name % 1 == 0) {
150
- name = "";
151
- }
152
- if (navStat % 1 == 0) {
153
- navStat = "";
160
+ function createTagBlock (aisTime) {
161
+ let tagBlock = ''
162
+ tagBlock += 's:SK0001,'
163
+ //tagBlock += 'c:' + aisTime + ','
164
+ tagBlock += 'c:' + Date.now(aisTime) + ','
165
+ tagBlock = tagBlock.slice(0, - 1)
166
+ let tagBlockChecksum = 0
167
+ for (let i = 0; i < tagBlock.length; i++) {
168
+ tagBlockChecksum ^= tagBlock.charCodeAt(i)
169
+ }
170
+ return `\\${tagBlock}*` + toHexString(tagBlockChecksum) + `\\`
171
+ }
172
+
173
+ //----------------------------------------------------------------------------
174
+ // Read and parse AIS data
175
+
176
+ readInfo = function readData(options) {
177
+ let i, mmsi, aisTime, aisDelay, shipName, lat, lon, sog, cog, rot, navStat, hdg, dst, callSign, imo, id, type;
178
+ let draftCur, length, beam, ais, encMsg3, encMsg5, encMsg18, encMsg240, encMsg241, own;
179
+ let ownLat = app.getSelfPath('navigation.position.value.latitude');
180
+ let ownLon = app.getSelfPath('navigation.position.value.longitude');
181
+ fetch(url, { method: 'GET' })
182
+ .then((res) => res.json())
183
+ .then((json) => {
184
+ const jsonContent = JSON.parse(JSON.stringify(json));
185
+ const numberAIS = Object.keys(jsonContent).length;
186
+ for (i = 0; i < numberAIS; i++) {
187
+ const jsonKey = Object.keys(jsonContent)[i];
188
+
189
+ try {
190
+ aisTime = jsonContent[jsonKey].sensors.ais.class.timestamp;
191
+ if ((parseFloat((moment(new Date(Date.now())).diff(aisTime)/1000).toFixed(3))) < position_update) {
192
+ aisDelay = true;
193
+ } else {
194
+ aisDelay = false;
154
195
  }
155
- if (dst % 1 == 0) {
156
- dst = "";
196
+ } catch (error) {
197
+ if (i === 0) {
198
+ aisTime = new Date(Date.now()).toISOString();
199
+ aisDelay = true;
200
+ } else {
201
+ aisTime = null;
202
+ aisDelay = false;
157
203
  }
158
- if (callSign % 1 == 0) {
159
- callSign = "";
204
+ }
205
+ try {
206
+ mmsi = jsonContent[jsonKey].mmsi;
207
+ } catch (error) { mmsi = null; }
208
+ try {
209
+ shipName = jsonContent[jsonKey].name;
210
+ } catch (error) { shipName = ''; }
211
+ try {
212
+ lat = jsonContent[jsonKey].navigation.position.value.latitude;
213
+ } catch (error) { lat = null; }
214
+ try {
215
+ lon = jsonContent[jsonKey].navigation.position.value.longitude;
216
+ } catch (error) { lon = null; }
217
+ try {
218
+ sog = msToKnots(jsonContent[jsonKey].navigation.speedOverGround.value);
219
+ } catch (error) { sog = null; }
220
+ try {
221
+ cog = radToDegrees(jsonContent[jsonKey].navigation.courseOverGroundTrue.value);
222
+ } catch (error) { cog = null; }
223
+ try {
224
+ rot = radToDegrees(jsonContent[jsonKey].navigation.rateOfTurn.value);
225
+ } catch (error) { rot = null; }
226
+ try {
227
+ navStat = stateMapping[jsonContent[jsonKey].navigation.state.value];
228
+ } catch (error) { navStat = ''; }
229
+ try {
230
+ hdg = radToDegrees(jsonContent[jsonKey].navigation.headingTrue.value);
231
+ } catch (error) { hdg = null; }
232
+ try {
233
+ dst = jsonContent[jsonKey].navigation.destination.commonName.value;
234
+ } catch (error) { dst = ''; }
235
+ try {
236
+ if (i === 0) {
237
+ callSign = jsonContent[jsonKey].communication.callsignVhf;
238
+ } else {
239
+ callSign = jsonContent[jsonKey].communication.value.callsignVhf;
160
240
  }
161
- if (type % 1 == 0) {
162
- type = "";
241
+ } catch (error) { callSign = ''; }
242
+ try {
243
+ imo = (jsonContent[jsonKey].registrations.value.imo).substring(4, 20);
244
+ } catch (error) { imo = null; }
245
+ try {
246
+ id = jsonContent[jsonKey].design.aisShipType.value.id;
247
+ } catch (error) { id = null; }
248
+ try {
249
+ type = jsonContent[jsonKey].design.aisShipType.value.name;
250
+ } catch (error) { type = ''; }
251
+ try {
252
+ draftCur = (jsonContent[jsonKey].design.draft.value.current) / 10;
253
+ } catch (error) { draftCur = null; }
254
+ try {
255
+ length = jsonContent[jsonKey].design.length.value.overall;
256
+ } catch (error) { length = null; }
257
+ try {
258
+ beam = (jsonContent[jsonKey].design.beam.value) / 2;
259
+ } catch (error) { beam = null; }
260
+ try {
261
+ ais = jsonContent[jsonKey].sensors.ais.class.value;
262
+ } catch (error) { ais = null; }
263
+
264
+ if (shipName % 1 === 0) {
265
+ shipName = '';
266
+ }
267
+ if (dst % 1 === 0) {
268
+ dst = '';
269
+ }
270
+ if (callSign % 1 === 0) {
271
+ callSign = '';
272
+ }
273
+ if (type % 1 === 0) {
274
+ type = '';
275
+ }
276
+
277
+ if (i === 0) {
278
+ own = true;
279
+ if (sendOwn) {
280
+ ais = 'A';
281
+ } else {
282
+ ais = '';
163
283
  }
284
+ } else {
285
+ own = false;
286
+ }
287
+
288
+ const a = { lat: ownLat, lon: ownLon }
289
+ const b = { lat: lat, lon: lon }
290
+ let dist = (haversine(a, b)/1000).toFixed(2);
164
291
 
165
- enc_msg_3 = {
292
+ if (dist <= distance) {
293
+
294
+ encMsg3 = {
295
+ own,
166
296
  aistype: 3, // class A position report
167
297
  repeat: 0,
168
- mmsi: mmsi,
298
+ mmsi,
169
299
  navstatus: navStat,
170
- sog: sog,
171
- lon: lon,
172
- lat: lat,
173
- cog: cog,
174
- hdg: hdg,
175
- rot: rot
176
- }
177
-
178
- enc_msg_5 = {
179
- aistype: 5, //class A static
300
+ sog,
301
+ lon,
302
+ lat,
303
+ cog,
304
+ hdg,
305
+ rot,
306
+ };
307
+
308
+ encMsg5 = {
309
+ own,
310
+ aistype: 5, // class A static
180
311
  repeat: 0,
181
- mmsi: mmsi,
182
- imo: imo,
312
+ mmsi,
313
+ imo,
183
314
  cargo: id,
184
315
  callsign: callSign,
185
- shipname: name,
186
- draught: draft_cur,
316
+ shipname: shipName,
317
+ draught: draftCur,
187
318
  destination: dst,
188
319
  dimA: 0,
189
320
  dimB: length,
190
321
  dimC: beam,
191
- dimD: beam
192
- }
193
-
194
- enc_msg_18 = {
322
+ dimD: beam,
323
+ };
324
+
325
+ encMsg18 = {
326
+ own,
195
327
  aistype: 18, // class B position report
196
328
  repeat: 0,
197
- mmsi: mmsi,
198
- sog: sog,
329
+ mmsi,
330
+ sog,
199
331
  accuracy: 0,
200
- lon: lon,
201
- lat: lat,
202
- cog: cog,
203
- hdg: hdg
204
- }
205
-
206
- enc_msg_24_0 = {
332
+ lon,
333
+ lat,
334
+ cog,
335
+ hdg,
336
+ };
337
+
338
+ encMsg240 = {
339
+ own,
207
340
  aistype: 24, // class B static
208
341
  repeat: 0,
209
342
  part: 0,
210
- mmsi: mmsi,
211
- shipname: name
212
- }
213
-
214
- enc_msg_24_1 = {
343
+ mmsi,
344
+ shipname: shipName,
345
+ };
346
+
347
+ encMsg241 = {
348
+ own,
215
349
  aistype: 24, // class B static
216
350
  repeat: 0,
217
351
  part: 1,
218
- mmsi: mmsi,
352
+ mmsi,
219
353
  cargo: id,
220
354
  callsign: callSign,
221
355
  dimA: 0,
222
356
  dimB: length,
223
357
  dimC: beam,
224
- dimD: beam
225
- }
358
+ dimD: beam,
359
+ };
360
+
361
+ if (aisDelay && (ais === 'A' || ais === 'B')) {
362
+ app.debug("Distance range: " + distance + "km, AIS target distance: " + dist + "km" + ", Class " + ais + " Vessel" + ", MMSI:" + mmsi)
363
+ if (ais === 'A') {
364
+ app.debug(`class A, ${i}, time: ${aisTime}`);
365
+ aisOut(encMsg3, aisTime);
366
+ aisOut(encMsg5, aisTime);
367
+ }
368
+ if (ais === 'B') {
369
+ app.debug(`class B, ${i}, time: ${aisTime}`);
370
+ aisOut(encMsg18, aisTime);
371
+ aisOut(encMsg240, aisTime);
372
+ aisOut(encMsg241, aisTime);
373
+ }
374
+ app.debug("--------------------------------------------------------");
226
375
 
227
- if (ais == "A") {
228
- app.debug("class A " + i);
229
- ais_out(enc_msg_3);
230
- ais_out(enc_msg_5);
231
- }
232
- if (ais == "B") {
233
- app.debug("class B " + i);
234
- ais_out(enc_msg_18);
235
- ais_out(enc_msg_24_0);
236
- ais_out(enc_msg_24_1);
237
376
  }
238
-
239
377
  }
240
- var dateobj = new Date( Date.now());
241
- var date = dateobj.toISOString();
378
+ }
379
+ const dateobj = new Date(Date.now());
380
+ const date = dateobj.toISOString();
242
381
  app.handleMessage(plugin.id, {
243
382
  context: `vessels.${app.selfId}`,
244
383
  updates: [
245
- ]
384
+ ],
246
385
  });
247
- setStatus(`Number of AIS targets sent: ${numberAIS-1} (${date})`);
248
- })
249
- .catch(err => console.error(err));
250
-
386
+ setStatus(`Number of AIS targets sent: ${numberAIS - 1} (${date})`);
387
+ })
388
+ .catch((err) => console.error(err));
251
389
  };
252
390
 
253
- //----------------------------------------------------------------------------
391
+ //----------------------------------------------------------------------------
254
392
 
255
393
  plugin.stop = function stop() {
256
- clearInterval(interval_id2);
394
+ clearInterval(intervalRun);
257
395
  app.debug('Stopped');
258
396
  };
259
397
 
@@ -261,10 +399,35 @@ function ais_out(enc_msg) {
261
399
  type: 'object',
262
400
  properties: {
263
401
  position_update: {
264
- type: 'integer',
402
+ type: 'number',
265
403
  default: 1,
266
- title: 'How often AIS data is sent to NMEA0183 out (in minutes)',
267
- }
404
+ title: 'How often AIS data is sent to NMEA0183 out (in minutes). E.g. 0.5 = 30s, 1 = 1min',
405
+ },
406
+ port: {
407
+ type: 'number',
408
+ title: 'HTTP port',
409
+ default: 3000
410
+ },
411
+ portSec: {
412
+ type: 'number',
413
+ title: 'HTTPS port',
414
+ default: 3443
415
+ },
416
+ sendOwn: {
417
+ type: 'boolean',
418
+ title: 'Send own AIS data, VDO',
419
+ default: true
420
+ },
421
+ useTag: {
422
+ type: 'boolean',
423
+ title: 'Add Tag-block',
424
+ default: false
425
+ },
426
+ distance: {
427
+ type: 'integer',
428
+ default: 100,
429
+ title: 'AIS target within range [km]',
430
+ },
268
431
  },
269
432
  };
270
433
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-vessels-to-ais",
3
- "version": "1.1.1",
3
+ "version": "1.1.5",
4
4
  "description": "SignalK server plugin to convert other vessel data to NMEA0183 AIS format and forward it out to 3rd party applications",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -13,7 +13,14 @@
13
13
  "author": "Karl-Erik Gustafsson",
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
- "node-fetch": "^2.6.0",
17
- "ggencoder": "^1.0.4"
16
+ "haversine-distance": "^1.2.1",
17
+ "moment": "^2.29.1",
18
+ "node-fetch": "^3.1.1",
19
+ "ggencoder": "^1.0.5"
20
+ },
21
+ "devDependencies": {
22
+ "eslint": "^7.17.0",
23
+ "eslint-config-airbnb-base": "^14.2.1",
24
+ "eslint-plugin-import": "^2.22.1"
18
25
  }
19
26
  }