1 | /* |
||
2 | * Copyright 2016, Teppo Kurki |
||
3 | * |
||
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
||
5 | * you may not use this file except in compliance with the License. |
||
6 | * You may obtain a copy of the License at |
||
7 | * |
||
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
||
9 | * |
||
10 | * Unless required by applicable law or agreed to in writing, software |
||
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
||
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||
13 | * See the License for the specific language governing permissions and |
||
14 | * limitations under the License. |
||
15 | * |
||
16 | */ |
||
17 | |||
18 | var _ = require('lodash') |
||
19 | var signalkSchema = require('./') // TODO should not be needed |
||
20 | var getId |
||
21 | var debug = require('debug')('signalk:fullsignalk') |
||
22 | |||
23 | function FullSignalK (id, type, defaults) { |
||
24 | // hack, apparently not available initially, so need to set lazily |
||
25 | getId = signalkSchema.getSourceId |
||
26 | |||
27 | if (!type || ((type !== 'aton') && (type !== 'aircraft') && (type !== 'sar'))) { type = 'vessels' } // vessels is the default if the passed value is anything invalid |
||
28 | |||
29 | this.root = {} |
||
30 | this.root.version = '1.0.2' // Should we read this from the package.json file? |
||
31 | this.root.self = '' |
||
32 | this.root[type] = {} |
||
33 | |||
34 | if (id) { |
||
35 | this.root[type][id] = defaults && defaults.vessels && defaults.vessels.self ? defaults.vessels.self : {} |
||
36 | this.self = this.root[type][id] |
||
37 | fillIdentity(this.root) |
||
38 | this.root.self = type + '.' + id |
||
39 | } |
||
40 | |||
41 | this.sources = {} |
||
42 | this.root.sources = this.sources |
||
43 | this.lastModifieds = {} |
||
44 | } |
||
45 | |||
46 | function fillIdentity (full) { |
||
47 | // looks at all members of vessels, aton, aircraft and sar, takes their property name and |
||
48 | // ensures there is one of mmsi, uuid or url within that branch as appropriate |
||
49 | const groups = ['vessels', 'aton', 'aircraft', 'sar'] |
||
50 | for (var g = 0; g < groups.length; g++) { |
||
51 | if (full.hasOwnProperty(groups[g])) { |
||
52 | for (var identity in full[groups[g]]) { |
||
0 ignored issues
–
show
|
|||
53 | fillIdentityField(full[groups[g]][identity], identity) |
||
54 | // fill arbitrarily the last id as self, used in tests |
||
55 | full.self = identity // TODO this shouldn't be here it would unexpectedly change the self |
||
56 | } |
||
57 | } |
||
58 | } |
||
59 | } |
||
60 | |||
61 | function fillIdentityField (vesselData, identity) { |
||
62 | // accepts a vessel/aton/sar/aircraft branch and the id of that branch and makes sure there is one of |
||
63 | // mmsi, uuid or url within it |
||
64 | // eg. fillIdentityField({}, 'urn:mrn:imo:mmsi:230099999') returns {"mmsi": "230099999"} |
||
65 | const mmsiPrefix = 'urn:mrn:imo:mmsi:' |
||
66 | if (identity.indexOf(mmsiPrefix) === 0) { |
||
67 | vesselData.mmsi = identity.substring(mmsiPrefix.length, identity.length) |
||
68 | } else if (identity.indexOf('urn:mrn:signalk') === 0) { |
||
69 | vesselData.uuid = identity |
||
70 | } else { |
||
71 | vesselData.url = identity |
||
72 | } |
||
73 | } |
||
74 | |||
75 | require('util').inherits(FullSignalK, require('events').EventEmitter) |
||
76 | |||
77 | FullSignalK.prototype.retrieve = function () { |
||
78 | return this.root |
||
79 | } |
||
80 | |||
81 | FullSignalK.prototype.addDelta = function (delta) { |
||
82 | this.emit('delta', delta) |
||
83 | var context = findContext(this.root, delta.context) |
||
84 | this.addUpdates(context, delta.context, delta.updates) |
||
85 | this.updateLastModified(delta.context) |
||
86 | } |
||
87 | |||
88 | FullSignalK.prototype.updateLastModified = function (contextKey) { |
||
89 | this.lastModifieds[contextKey] = new Date().getTime() |
||
90 | } |
||
91 | |||
92 | FullSignalK.prototype.pruneContexts = function (seconds) { |
||
93 | var threshold = new Date().getTime() - seconds * 1000 |
||
94 | for (let contextKey in this.lastModifieds) { |
||
95 | if (this.lastModifieds[contextKey] < threshold) { |
||
96 | this.deleteContext(contextKey) |
||
97 | delete this.lastModifieds[contextKey] |
||
98 | } |
||
99 | } |
||
100 | } |
||
101 | |||
102 | FullSignalK.prototype.deleteContext = function (contextKey) { |
||
103 | debug('Deleting context ' + contextKey) |
||
104 | var pathParts = contextKey.split('.') |
||
105 | if (pathParts.length === 2) { |
||
106 | delete this.root[pathParts[0]][pathParts[1]] |
||
107 | } |
||
108 | } |
||
109 | |||
110 | function findContext (root, contextPath) { |
||
111 | var context = _.get(root, contextPath) |
||
112 | if (!context) { |
||
113 | context = {} |
||
114 | _.set(root, contextPath, context) |
||
115 | } |
||
116 | var identity = contextPath.split('.')[1] |
||
117 | if (!identity) { |
||
118 | return undefined |
||
119 | } |
||
120 | fillIdentityField(context, identity) |
||
121 | return context |
||
122 | } |
||
123 | |||
124 | FullSignalK.prototype.addUpdates = function (context, contextPath, updates) { |
||
125 | var len = updates.length |
||
126 | for (var i = 0; i < len; ++i) { |
||
127 | this.addUpdate(context, contextPath, updates[i]) |
||
128 | } |
||
129 | } |
||
130 | |||
131 | FullSignalK.prototype.addUpdate = function (context, contextPath, update) { |
||
132 | if (typeof update.source !== 'undefined') { |
||
133 | this.updateSource(context, update.source, update.timestamp) |
||
134 | } else if (typeof update['$source'] !== 'undefined') { |
||
135 | this.updateDollarSource(context, update['$source'], update.timestamp) |
||
136 | } else { |
||
137 | console.error('No source in delta update:' + JSON.stringify(update)) |
||
138 | } |
||
139 | addValues(context, contextPath, update.source || update['$source'], update.timestamp, update.values) |
||
140 | } |
||
141 | |||
142 | FullSignalK.prototype.updateDollarSource = function (context, dollarSource, timestamp) { |
||
0 ignored issues
–
show
|
|||
143 | const parts = dollarSource.split('.') |
||
144 | parts.reduce((cursor, part) => { |
||
145 | if (typeof cursor[part] === 'undefined') { |
||
146 | cursor[part] = {} |
||
147 | } |
||
148 | return cursor[part] |
||
149 | }, this.sources) |
||
150 | } |
||
151 | |||
152 | FullSignalK.prototype.updateSource = function (context, source, timestamp) { |
||
153 | if (!this.sources[source.label]) { |
||
154 | this.sources[source.label] = {} |
||
155 | this.sources[source.label].label = source.label |
||
156 | this.sources[source.label].type = source.type |
||
157 | } |
||
158 | |||
159 | if (source.type === 'NMEA2000' || source.src) { |
||
160 | handleNmea2000Source(this.sources[source.label], source, timestamp) |
||
161 | return |
||
162 | } |
||
163 | |||
164 | if (source.type === 'NMEA0183' || source.sentence) { |
||
165 | handleNmea0183Source(this.sources[source.label], source, timestamp) |
||
166 | return |
||
167 | } |
||
168 | |||
169 | handleOtherSource(this.sources[source.label], source, timestamp) |
||
170 | } |
||
171 | |||
172 | function handleNmea2000Source (labelSource, source, timestamp) { |
||
173 | if (!labelSource[source.src]) { |
||
174 | labelSource[source.src] = { |
||
175 | n2k: { |
||
176 | src: source.src, |
||
177 | pgns: {} |
||
178 | } |
||
179 | } |
||
180 | } |
||
181 | if (source.instance && !labelSource[source.src][source.instance]) { |
||
182 | labelSource[source.src][source.instance] = {} |
||
183 | } |
||
184 | labelSource[source.src].n2k.pgns[source.pgn] = timestamp |
||
185 | } |
||
186 | |||
187 | function handleNmea0183Source (labelSource, source, timestamp) { |
||
188 | var talker = source.talker || 'II' |
||
189 | if (!labelSource[talker]) { |
||
190 | labelSource[talker] = { |
||
191 | talker: talker, |
||
192 | sentences: {} |
||
193 | } |
||
194 | } |
||
195 | labelSource[talker].sentences[source.sentence] = timestamp |
||
196 | } |
||
197 | |||
198 | function handleOtherSource (sourceLeaf, source, timestamp) { |
||
199 | sourceLeaf.timestamp = timestamp |
||
200 | } |
||
201 | |||
202 | function addValues (context, contextPath, source, timestamp, pathValues) { |
||
203 | var len = pathValues.length |
||
204 | for (var i = 0; i < len; ++i) { |
||
205 | addValue(context, contextPath, source, timestamp, pathValues[i]) |
||
206 | } |
||
207 | } |
||
208 | |||
209 | function addValue (context, contextPath, source, timestamp, pathValue) { |
||
210 | if (_.isUndefined(pathValue.path) || _.isUndefined(pathValue.value)) { |
||
211 | console.error('Illegal value in delta:' + JSON.stringify(pathValue)) |
||
212 | return |
||
213 | } |
||
214 | var valueLeaf |
||
215 | if (pathValue.path.length === 0) { |
||
216 | _.merge(context, pathValue.value) |
||
217 | return |
||
218 | } else { |
||
219 | const splitPath = pathValue.path.split('.') |
||
220 | valueLeaf = splitPath.reduce(function (previous, pathPart, i) { |
||
221 | if (!previous[pathPart]) { |
||
222 | previous[pathPart] = {} |
||
223 | let meta = signalkSchema.getMetadata(contextPath + '.' + pathValue.path) |
||
224 | if (meta && i === splitPath.length - 1) { |
||
225 | // ignore properties from keyswithmetadata.json |
||
226 | meta = JSON.parse(JSON.stringify(meta)) |
||
227 | delete meta.properties |
||
228 | |||
229 | previous[pathPart].meta = meta |
||
230 | } |
||
231 | } |
||
232 | return previous[pathPart] |
||
233 | }, context) |
||
234 | } |
||
235 | |||
236 | var sourceId |
||
237 | if (valueLeaf.values) { // multiple values already |
||
238 | sourceId = getId(source) |
||
239 | if (!valueLeaf.values[sourceId]) { |
||
240 | valueLeaf.values[sourceId] = {} |
||
241 | } |
||
242 | assignValueToLeaf(pathValue.value, valueLeaf.values[sourceId]) |
||
243 | valueLeaf.values[sourceId].timestamp = timestamp |
||
244 | setMessage(valueLeaf.values[sourceId], source) |
||
245 | } else if (typeof valueLeaf.value !== 'undefined' && valueLeaf['$source'] !== getId(source)) { |
||
246 | // first multiple value |
||
247 | |||
248 | sourceId = valueLeaf['$source'] |
||
249 | var tmp = {} |
||
250 | copyLeafValueToLeaf(valueLeaf, tmp) |
||
251 | valueLeaf.values = {} |
||
252 | valueLeaf.values[sourceId] = tmp |
||
253 | valueLeaf.values[sourceId].timestamp = valueLeaf.timestamp |
||
254 | |||
255 | sourceId = getId(source) |
||
256 | valueLeaf.values[sourceId] = {} |
||
257 | assignValueToLeaf(pathValue.value, valueLeaf.values[sourceId]) |
||
258 | valueLeaf.values[sourceId].timestamp = timestamp |
||
259 | setMessage(valueLeaf.values[sourceId], source) |
||
260 | } |
||
261 | assignValueToLeaf(pathValue.value, valueLeaf) |
||
262 | if (pathValue.path.length !== 0) { |
||
263 | valueLeaf['$source'] = getId(source) |
||
264 | valueLeaf.timestamp = timestamp |
||
265 | setMessage(valueLeaf, source) |
||
266 | } |
||
267 | } |
||
268 | |||
269 | function copyLeafValueToLeaf (fromLeaf, toLeaf) { |
||
270 | _.assign(toLeaf, _.omit(fromLeaf, ['$source', 'timestamp', 'meta'])) |
||
271 | } |
||
272 | |||
273 | function assignValueToLeaf (value, leaf) { |
||
274 | leaf.value = value |
||
275 | } |
||
276 | |||
277 | function setMessage (leaf, source) { |
||
278 | if (!source) { |
||
279 | return |
||
280 | } |
||
281 | if (source.pgn) { |
||
282 | leaf.pgn = source.pgn |
||
283 | delete leaf.sentence |
||
284 | } |
||
285 | if (source.sentence) { |
||
286 | leaf.sentence = source.sentence |
||
287 | delete leaf.pgn |
||
288 | } |
||
289 | } |
||
290 | |||
291 | module.exports = FullSignalK |
||
292 |
When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically: