├── .babelrc ├── .flowconfig ├── .gitignore ├── Jenkinsfile ├── Jenkinsfile.alt ├── README.md ├── SupportedValues.md ├── docker └── Dockerfile ├── index.js ├── lib ├── Ais04Msg.js ├── Ais05Msg.js ├── Ais18Msg.js ├── Ais19Msg.js ├── Ais21Msg.js ├── Ais24Msg.js ├── Ais27Msg.js ├── AisBitField.js ├── AisCNBMsg.js ├── AisMessage.js └── AisParser.js ├── makefile ├── package-lock.json ├── package.json ├── samples └── Sample.js ├── src ├── Ais04Msg.js ├── Ais05Msg.js ├── Ais18Msg.js ├── Ais19Msg.js ├── Ais21Msg.js ├── Ais24Msg.js ├── Ais27Msg.js ├── AisBitField.js ├── AisCNBMsg.js ├── AisMessage.js ├── AisParser.js └── __tests__ │ ├── AisBitFieldGetInt.js │ ├── AisBitFieldGetString.js │ ├── AisParserTest.js │ └── testHelper │ └── AisBitfieldDataGenerator.js ├── test-data ├── ais00835.log ├── ais28736.log └── nmea-sample ├── test.log └── test ├── .gitignore └── scanFile.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-flow-strip-types"], 3 | "presets": ["env"] 4 | } 5 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [options] 2 | unsafe.enable_getters_and_setters=true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !/test/ais*.log 2 | /test/*.log 3 | docs 4 | stuff 5 | node_modules 6 | .idea 7 | yarn-error.log 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | image 'samothx/node-dev:latest' 5 | args '-v yarn_cache:/usr/local/share/.cache/yarn' 6 | } 7 | } 8 | 9 | environment { 10 | CI = 'true' 11 | } 12 | 13 | stages { 14 | 15 | stage('Build') { 16 | steps { 17 | sh 'yarn install' 18 | sh 'yarn run build' 19 | } 20 | } 21 | 22 | stage('Test') { 23 | steps { 24 | sh 'yarn run test' 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Jenkinsfile.alt: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | dockerfile { 4 | dir 'docker' 5 | args '-v yarn_cache:/usr/local/share/.cache/yarn' 6 | } 7 | } 8 | 9 | environment { 10 | CI = 'true' 11 | } 12 | 13 | stages { 14 | 15 | stage('Build') { 16 | steps { 17 | sh 'yarn install' 18 | sh 'yarn run build' 19 | } 20 | } 21 | 22 | stage('Test') { 23 | steps { 24 | sh 'yarn run test' 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AisParser 2 | A Parser for NMEA0183 AIS messages. 3 | 4 | ## Installation 5 | The parser is written using [flow](https://flowtype.org/). It can be run from the src directory with babel-node or in the transpiled version from the index.js file or the lib directory. If you are using the NPM package ( add "aisparser" :">=0.0.12" to your package.json dependencies) you do not have to worry about transpiling, it has been done for you allready. If you are using the github package you will need to take care of transpiling by calling the following commands: 6 | ``` 7 | cd 8 | npm install 9 | npm run-script transpile 10 | ``` 11 | 12 | ## How it works 13 | The modules approach to parsing AIS messages is 'on demand'. A message is merely stored and some basic checks are done by the **parse** function. When data is requested only as much of the message is parsed as is needed to decode the requested data. For instance when the aisType is read only one byte of the message is actually translated and parsed. So it makes sense to only read the values that are really needed. Although some common values are cached in the result object once they have been requested, most values are not - meaning that they are parsed every time they are requested. 14 | 15 | The Module parses AIS messages of types 1,2,3.4.5,18,19,21 and 24. These are the common message types, most other types are related to inter vessel or vessel to shore communication. 16 | 17 | Although the parser has been thoroughly checked against AIS logs from AISHub and AIS recordings from the Panama Canal, the author takes no responsibility for the correctness of returned values. Please always keep a good watch and an eye on the traffic while commanding a vessel. 18 | 19 | The result object obtained from the parse function has a variable **supportedValues** which returns an object containing the field names that can be retrieved from the result object associated with their type or unit. The list may look something like this: 20 | ```json 21 | { 22 | "aisType" : "number", 23 | "mmsi" : "number", 24 | "name" : "string", 25 | "longitude" : "deg", 26 | "latitude" : "deg", 27 | "sog" : "kn" 28 | ... 29 | } 30 | ``` 31 | The list is specific to the message type, it lists values that may be present in the message. Retrieving the values may still return NaN or "" values, if the value is set to empty or undefined in the actual message. 32 | 33 | The instance variables of the result object are implemented as getter functions: The parsing is done while the instance variables are accessed and they can throw exceptions when parsing fails. This should only happen when maformed (too short) messages are being processed. Having the checksum checked should make sure that this does not happen as long as the device producing the messages does not emmit faulty messages. Otherwise use a try catch block around the data retrieval to catch parse exceptions. 34 | 35 | ## API 36 | 37 | ### Constructor 38 | ```new AisParser(options)``` 39 | 40 | Create a parser object, the parameter options can be omitted. 41 | When supplied currently only one parameter ```checksum``` of type boolean is supported. If checksum is set to true AIS checksums are checked prior to parsing a message. 42 | 43 | #### Example: 44 | ```var parser = new AisParser({ checksum : true })``` 45 | 46 | ### Function checksumValid(message) 47 | checksumValid is a static function of the module, it calculates and compares the checksum and returns true if the value matches or false if not. The function also does some basic checks and returns false if the message is obviously not valid otherwise. 48 | 49 | #### Parameters 50 | The function takes one string parameter: 51 | - The AIS message. 52 | 53 | 54 | #### Example: 55 | ```var msgOk = AisParser.checksumValid('!AIVDM,1,1,,B,14`c;d002grD>PH50hr7RVE000SG,0*74')``` 56 | 57 | ### Function parse(message,options) 58 | The Function takes two parameters: 59 | - The parameter message supplies the NMEA0183 AIS message to be parsed. 60 | - The second parameter can be left out. It has the same content as the constructors option parameter. When given it overrides the options given in the constructor. 61 | 62 | #### Return Value 63 | The function returns a result object that can be used to retrieve the status of the parse process and the messages values when parsing was successful. 64 | 65 | ### Function parseArray(array) 66 | The function takes a preprocessed message as parameter. The array can be derived from the original message by splitting the 67 | the message (eg. using message.split(',')) and is targeted at environments where NMEA0183 messages have already preprocessed handing them to the AIS parser. This function does not compute a checksum. 68 | 69 | #### Return Value 70 | The function returns a result object that can be used to retrieve the status of the parse process and the messages values when parsing was successful. 71 | 72 | ### The Result Object 73 | The result object contains the semi parsed message and a status of the parse process. Before reading any other values the **valid** instance variable must be read. It contains the status as a string and can be either of 74 | - **VALID** - the message is valid and complete and further values can be read. 75 | - **INVALID** - the message could not be parsed. Further information can be optained by reading the **errMsg** variable. 76 | - **UNSUPPORTED** - the message was either of unsupported aisType or not an AIS message. Further information can be optained by reading the **errMsg** variable. 77 | - **INCOMPLETE** - the message is part of a sequence of messages. Only when the last message of the sequence has been parsed will results be returned. 78 | 79 | Generally all possible values can be queried on a valid or invalid result object. Numerical values will result in **NaN**, string values in the **empty string** when not part of the message. Only available values will actually be parsed. If errors occurr during parsing you will receive an exception, so it is important to secure the reading code with a try catch block. 80 | 81 | The **supportedValues** variable supplies a dictionary of strings that contains the variable names that are supported by the parsed message associated with their type or unit. 82 | 83 | The function ***getUnit(fieldName)*** returns the type of value for a field. 84 | For numeric values following return values are possible: 85 | - 'number' - a plain number. 86 | - 'index' - an index into a list or enum. These values often have an accompanying field to get the string associated with the numberic value eg. epfd -> index, epfdStr returns the string. 87 | - 'string' - a string value. 88 | - unit - a unit name like 'm', 'deg','rad'. 89 | 90 | 91 | The following variables are available for all message types: 92 | - ***aisType*** - the ais message type. 93 | - ***channel*** - the channel on which the message was sent, either 'A', 'B' or ''. 94 | - ***repeatInd*** - The Repeat Indicator is a directive to an AIS transceiver that this 95 | message should be rebroadcast. 96 | - ***mmsi*** - the Maritime Mobile Service Identity. 97 | 98 | A complete list of parameters can be found in SupportedValues.md. 99 | 100 | ## Testing 101 | 102 | You will find the files scanFile.js and testdata.tgz in the test directory. 103 | When AisParser is installed via 'npm install' it can be run with babel-node. 104 | 105 | ```shell 106 | tar -xzf testdata.tgz 107 | babel-node scanFile.js output.txt output.csv output.fail plain 108 | ``` 109 | 110 | The command will scan the file output1000.txt print all errors to the screen. There will plenty of errors because the file contains about 1000 NMEA messages from the Panama Canal which are not all AIS messages- There are also several unsupported AIS messages in the file. With all supplied test files all errors should be of type UNSUPPORTED and refer to message types other than 1,2,3,4,5,18,19,21,24. 111 | After executing the command the file output1000.csv should contain comma separated values data with the content of the parsed messages. It can be opened with excel or Libreoffice Calc. The file output1000.fail will contain all failed AIS messages. 112 | The last parameter delivers the type of data to be read. When set to sigk it will try to parse a format delivered by the signalk-node-server that puts a timestamp and a source tag in front of every line. 113 | 114 | ## Usage: (as in samples/Sample.js) 115 | ```javascript 116 | var AisParser = require('../index'); 117 | 118 | var parser = new AisParser({ checksum : true }); 119 | 120 | var sentences = [ 121 | '!AIVDM,1,1,,B,14`c;d002grD>PH50hr7RVE000SG,0*74', 122 | '!AIVDM,1,1,,B,34hwN60Oh3rCwib56`qJtbL<0000,0*12', 123 | '!AIVDM,1,1,,B,15TPq@0Oj0rClEv53P9HWVn<283C,0*51', 124 | '!AIVDO,1,1,,,B39i>1000nTu;gQAlBj:wwS5kP06,0*5D', 125 | '!AIVDM,1,1,,A,35SrP>5P00rCQAL5:KA8b0000rCgTH58DU6KpJj0`0>,0*37', 130 | '!AIVDO,1,1,,,B39i>1001FTu;bQAlAMscwe5kP06,0*3E', 131 | '!AIVDM,1,1,,B,15Bs:H8001JCUE852dB10m|true,false|1, 2, 3, 4, 18, 19| 17 | |navStatus|index|Navigational Status of AIS Target|0-15|1,2,3| 18 | |navStatusStr|string|A String associated with Nav Status|-|1,2,3| 19 | |utcYear|year|Message Type 4 Base Station Time Reference|1-999, 0=N/A|4| 20 | |utcMonth|month|Message Type 4 Base Station Time Reference|1-12, 0=N/A|4| 21 | |utcDay|day|Message Type 4 Base Station Time Reference|1-31, 0=N/A|4| 22 | |utcHour|h|Message Type 4 Base Station Time Reference|0-23, 24 = N/A|4| 23 | |utcMinute|min|Message Type 4 Base Station Time Reference|0-59, 60=N/A|4| 24 | |utcSecond|s|Message Type 4 Base Station Time Reference|0-59, 60=N/A|4| 25 | |epfd|index|EPFD Fix Type|0-8, 0=N/A|4,5,19| 26 | |epfdStr|string|A String associated with EPFD index|-|4,5,19| 27 | |callSign|string|Callsign of the AIS Target|7 Characters|5,24| 28 | |name|string|AIS Targets Name|20 Characters|5,24| 29 | |aisVer|number|0=ITU1371, 1-3=future editions|0-3|5| 30 | |imo|number|IMO Registration Number|9 Digits|5| 31 | |shipType|index|Ship Type & Cargo||5,19.24| 32 | |shipTypeStr|string|A String associated with Ship Type & Cargo|-|5, 19, 24| 33 | |dimToBow|m|Distance of the GPS Receiverfrom the Bow|1-510|5, 19, 21, 24| 34 | |dimToBowStatus|string|Status of DimToBow, HUGE > 511|VALID, NA, HUGE|5, 19, 21, 24| 35 | |dimToStern|m|Distance of the GPS Receiverfrom the Stern|1-510|5, 19, 21, 24| 36 | |dimToSternStatus|string|Status of DimToStern, HUGE > 511|VALID, NA, HUGE|5, 19, 21, 24| 37 | |dimToStbrd|m|Distance of the GPS Receiverfrom the Stardboard Side|1-62|5, 19, 21, 24| 38 | |dimToStbrdStatus|string|Status of DimToStbrd, HUGE > 63|VALID, NA, HUGE|5, 19, 21, 24| 39 | |dimToPort|m|Distance of the GPS Receiverfrom the Port Side|1-62|5, 19, 21, 24| 40 | |dimToPortStatus|string|Status of DimToPort, HUGE > 63|VALID, NA, HUGE|5, 19, 21, 24| 41 | |etaMonth|month|UTC Month of ETA at Destination|1-12 0=N/A|5| 42 | |etaDay|day|UTC day of ETA at Destination|1-3 10=N/A|5| 43 | |etaHour|h|UTC Hour of ETA at Destination|0-59 60=N/A|5| 44 | |etaMinute|min|UTC Minute of ETA at Destination|0=N/A 0-59|5| 45 | |destination|string|Destination of Vessel|-|5| 46 | |draught|m|Draught of Target|-|5| 47 | |heading|deg|True Heading of Target|0-359|1, 2, 3, 18, 19| 48 | |sog|kn|Speed over Ground|0-102.1|1, 2, 3, 18, 19| 49 | |sogStatus|string|Status of Speed over Ground, if status is VALID, then sog contains the Speed |VALID,HIGH,NA|1, 2, 3, 18, 19| 50 | |cog|deg|Course over Ground|0-359|1, 2, 3, 18, 19| 51 | |utcTsSec|s|Seconds of UTC Time|0-59|1, 2, 3, 18, 19| 52 | |utcTsStatus|string|Status of the utcTsSec Paramerter|NA, MANUAL, ESTIMATED, INOPERATIVE or INVALID|1, 2, 3, 18, 19| 53 | |vendorId|string|Vendor Name of the AIS equipment|-|24| 54 | |mothershipMmsi|string|MMSI of Mothership|9 Digit Number|24| 55 | |rot|deg/min|Rate of Turn|-126-126|1, 2, 3| 56 | |rotStatus|string|Status of Rate of Turn|'NONE, RIGHT, LEFT, NA|1,2,3| 57 | |offPosInd|string|Off Position indicator for Aid to Navigation|IN_POS, OFF_POS, NA|21| 58 | |aidType|index|Type of the Aid to Navigation|0-31|21| 59 | |aidTypeStr|string|A String associated with the aidType|-|21| 60 | |nameExt|string|Name Extension for Aid to Navigation|-|21| 61 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | RUN apk update && apk --no-cache add build-base git 4 | 5 | VOLUME ["/usr/local/share/.cache/yarn"] 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/AisParser'); 2 | -------------------------------------------------------------------------------- /lib/Ais04Msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _AisBitField = require('./AisBitField'); 10 | 11 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 12 | 13 | var _AisMessage2 = require('./AisMessage'); 14 | 15 | var _AisMessage3 = _interopRequireDefault(_AisMessage2); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | /* 26 | * AisParser: A parser for NMEA0183 AIS messages. 27 | * Copyright (C) 2017 Thomas Runte . 28 | * 29 | * This program is free software: you can redistribute it and/or modify 30 | * it under the terms of the Apache License Version 2.0 as published by 31 | * Apache Software foundation. 32 | * 33 | * This program is distributed in the hope that it will be useful, 34 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 | * GNU General Public License for more details. 37 | * 38 | * You should have received a copy of the Apache License Version 2.0 39 | * along with this program. If not, see . 40 | */ 41 | 42 | var MOD_NAME = 'Ais04Msg'; 43 | var SUPPORTED_FIELDS = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'latitude', 'longitude', 'posAccuracy', 'utcYear', 'utcMonth', 'utcDay', 'utcHour', 'utcMinute', 'utcSecond', 'epfd']; 44 | 45 | var suppValuesValid = false; 46 | var suppValues = {}; 47 | 48 | /* 49 | |============================================================================== 50 | |Field |Len |Description |Member |T|Units 51 | |0-5 | 6 |Message Type |type |u|Constant: 4 52 | |6-7 | 2 |Repeat Indicator |repeat |u|As in Common Navigation Block 53 | |8-37 | 30 |MMSI |mmsi |u|9 decimal digits 54 | |38-51 | 14 |Year (UTC) |year |u|UTC, 1-999, 0 = N/A (default) 55 | |52-55 | 4 |Month (UTC) |month |u|1-12; 0 = N/A (default) 56 | |56-60 | 5 |Day (UTC) |day |u|1-31; 0 = N/A (default) 57 | |61-65 | 5 |Hour (UTC) |hour |u|0-23; 24 = N/A (default) 58 | |66-71 | 6 |Minute (UTC) |minute |u|0-59; 60 = N/A (default) 59 | |72-77 | 6 |Second (UTC) |second |u|0-59; 60 = N/A (default) 60 | |78-78 | 1 |Fix quality |accuracy |b|As in Common Navigation Block 61 | |79-106 | 28 |Longitude |lon |I4|As in Common Navigation Block 62 | |107-133 | 27 |Latitude |lat |I4|As in Common Navigation Block 63 | |134-137 | 4 |Type of EPFD |epfd |e|See "EPFD Fix Types" 64 | |138-147 | 10 |Spare | |x|Not used 65 | // TODO 66 | |148-148 | 1 |RAIM flag |raim |b|As for common navigation block 67 | |149-167 | 19 |SOTDMA state |radio |u|As in same bits for Type 1 68 | |============================================================================== 69 | */ 70 | 71 | var Ais04Msg = function (_AisMessage) { 72 | _inherits(Ais04Msg, _AisMessage); 73 | 74 | function Ais04Msg(aisType, bitField, channel) { 75 | _classCallCheck(this, Ais04Msg); 76 | 77 | // TODO: check bitcount 78 | var _this = _possibleConstructorReturn(this, (Ais04Msg.__proto__ || Object.getPrototypeOf(Ais04Msg)).call(this, aisType, bitField, channel)); 79 | 80 | if (bitField.bits >= 167) { 81 | _this._valid = 'VALID'; 82 | } else { 83 | _this._valid = 'INVALID'; 84 | _this._errMsg = 'invalid bitcount for type 04 msg:' + bitField.bits; 85 | } 86 | return _this; 87 | } 88 | 89 | _createClass(Ais04Msg, [{ 90 | key: '_getRawLat', 91 | value: function _getRawLat() { 92 | return this._bitField.getInt(107, 27, false); 93 | } 94 | }, { 95 | key: '_getRawLon', 96 | value: function _getRawLon() { 97 | return this._bitField.getInt(79, 28, false); 98 | } 99 | }, { 100 | key: 'supportedValues', 101 | get: function get() { 102 | if (!suppValuesValid) { 103 | SUPPORTED_FIELDS.forEach(function (field) { 104 | var unit = _AisMessage3.default.getUnit(field); 105 | if (unit) { 106 | suppValues[field] = unit; 107 | } else { 108 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 109 | } 110 | }); 111 | suppValuesValid = true; 112 | } 113 | return suppValues; 114 | } 115 | }, { 116 | key: 'utcYear', 117 | get: function get() { 118 | return this._bitField.getInt(38, 14, true); 119 | } 120 | }, { 121 | key: 'utcMonth', 122 | get: function get() { 123 | return this._bitField.getInt(52, 4, true); 124 | } 125 | }, { 126 | key: 'utcDay', 127 | get: function get() { 128 | return this._bitField.getInt(56, 5, true); 129 | } 130 | }, { 131 | key: 'utcHour', 132 | get: function get() { 133 | return this._bitField.getInt(61, 5, true); 134 | } 135 | }, { 136 | key: 'utcMinute', 137 | get: function get() { 138 | return this._bitField.getInt(66, 6, true); 139 | } 140 | }, { 141 | key: 'utcSecond', 142 | get: function get() { 143 | return this._bitField.getInt(72, 6, true); 144 | } 145 | }, { 146 | key: 'posAccuracy', 147 | get: function get() { 148 | return this._bitField.getInt(78, 1, true) === 1; 149 | } 150 | }, { 151 | key: 'epfd', 152 | get: function get() { 153 | return this._bitField.getInt(134, 4, true); 154 | } 155 | }]); 156 | 157 | return Ais04Msg; 158 | }(_AisMessage3.default); 159 | 160 | exports.default = Ais04Msg; 161 | -------------------------------------------------------------------------------- /lib/Ais05Msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _AisBitField = require('./AisBitField'); 10 | 11 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 12 | 13 | var _AisMessage2 = require('./AisMessage'); 14 | 15 | var _AisMessage3 = _interopRequireDefault(_AisMessage2); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | /* 26 | * AisParser: A parser for NMEA0183 AIS messages. 27 | * Copyright (C) 2017 Thomas Runte . 28 | * 29 | * This program is free software: you can redistribute it and/or modify 30 | * it under the terms of the Apache License Version 2.0 as published by 31 | * Apache Software foundation. 32 | * 33 | * This program is distributed in the hope that it will be useful, 34 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 | * GNU General Public License for more details. 37 | * 38 | * You should have received a copy of the Apache License Version 2.0 39 | * along with this program. If not, see . 40 | */ 41 | 42 | var MOD_NAME = 'Ais05Msg'; 43 | var SUPPORTED_FIELDS = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'callSign', 'name', 'aisVer', 'imo', 'shipType', 'shipTypeStr', 'dimToBow', 'dimToBowStatus', 'dimToStern', 'dimToSternStatus', 'dimToPort', 'dimToPortStatus', 'dimToStbrd', 'dimToStbrdStatus', 'epfd', 'epfdStr', 'etaMonth', 'etaDay', 'etaHour', 'etaMinute', 'draught', 'destination']; 44 | 45 | var suppValuesValid = false; 46 | var suppValues = {}; 47 | 48 | /* 49 | |============================================================================== 50 | |Field |Len |Description |Member/Type |T|Encoding 51 | |0-5 | 6 |Message Type |type |u|Constant: 5 52 | |6-7 | 2 |Repeat Indicator |repeat |u|Message repeat count 53 | |8-37 | 30 |MMSI |mmsi |u|9 digits 54 | |38-39 | 2 |AIS Version |ais_version |u|0=<>, 55 | 1-3 = future editions 56 | |40-69 | 30 |IMO Number |imo |u|IMO ship ID number 57 | |70-111 | 42 |Call Sign |callsign |t|7 six-bit characters 58 | |112-231 |120 |Vessel Name |shipname |t|20 six-bit characters 59 | |232-239 | 8 |Ship Type |shiptype |e|See "Codes for Ship Type" 60 | |240-248 | 9 |Dimension to Bow |to_bow |u|Meters 61 | |249-257 | 9 |Dimension to Stern |to_stern |u|Meters 62 | |258-263 | 6 |Dimension to Port |to_port |u|Meters 63 | |264-269 | 6 |Dimension to Starboard |to_starboard |u|Meters 64 | |270-273 | 4 |Position Fix Type |epfd |e|See "EPFD Fix Types" 65 | |274-277 | 4 |ETA month (UTC) |month |u|1-12, 0=N/A (default) 66 | |278-282 | 5 |ETA day (UTC) |day |u|1-31, 0=N/A (default) 67 | |283-287 | 5 |ETA hour (UTC) |hour |u|0-23, 24=N/A (default) 68 | |288-293 | 6 |ETA minute (UTC) |minute |u|0-59, 60=N/A (default) 69 | |294-301 | 8 |Draught |draught |U1|Meters/10 70 | |302-421 |120 |Destination |destination |t|20 6-bit characters 71 | TODO: 72 | |422-422 | 1 |DTE |dte |b|0=Data terminal ready, 73 | 1=Not ready (default). 74 | |423-423 | 1 |Spare | |x|Not used 75 | |============================================================================== 76 | */ 77 | 78 | var Ais05Msg = function (_AisMessage) { 79 | _inherits(Ais05Msg, _AisMessage); 80 | 81 | function Ais05Msg(aisType, bitField, channel) { 82 | _classCallCheck(this, Ais05Msg); 83 | 84 | var _this = _possibleConstructorReturn(this, (Ais05Msg.__proto__ || Object.getPrototypeOf(Ais05Msg)).call(this, aisType, bitField, channel)); 85 | 86 | if (bitField.bits >= 423) { 87 | _this._valid = 'VALID'; 88 | } else { 89 | _this._valid = 'INVALID'; 90 | _this._errMsg = 'invalid bitcount for type 05 msg:' + bitField.bits; 91 | } 92 | return _this; 93 | } 94 | 95 | _createClass(Ais05Msg, [{ 96 | key: '_getDimToBow', 97 | value: function _getDimToBow() { 98 | return this._bitField.getInt(240, 9, true); 99 | } 100 | }, { 101 | key: '_getDimToStern', 102 | value: function _getDimToStern() { 103 | return this._bitField.getInt(249, 9, true); 104 | } 105 | }, { 106 | key: '_getDimToPort', 107 | value: function _getDimToPort() { 108 | return this._bitField.getInt(258, 6, true); 109 | } 110 | }, { 111 | key: '_getDimToStbrd', 112 | value: function _getDimToStbrd() { 113 | return this._bitField.getInt(264, 6, true); 114 | } 115 | }, { 116 | key: 'supportedValues', 117 | get: function get() { 118 | if (!suppValuesValid) { 119 | SUPPORTED_FIELDS.forEach(function (field) { 120 | var unit = _AisMessage3.default.getUnit(field); 121 | if (unit) { 122 | suppValues[field] = unit; 123 | } else { 124 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 125 | } 126 | }); 127 | suppValuesValid = true; 128 | } 129 | return suppValues; 130 | } 131 | }, { 132 | key: 'callSign', 133 | get: function get() { 134 | return this._formatStr(this._bitField.getString(70, 42)); 135 | } 136 | }, { 137 | key: 'name', 138 | get: function get() { 139 | return this._formatStr(this._bitField.getString(112, 120)); 140 | } 141 | }, { 142 | key: 'aisVer', 143 | get: function get() { 144 | return this._bitField.getInt(38, 2, true); 145 | } 146 | }, { 147 | key: 'imo', 148 | get: function get() { 149 | return this._bitField.getInt(40, 30, true); 150 | } 151 | }, { 152 | key: 'shipType', 153 | get: function get() { 154 | return this._bitField.getInt(232, 8, true); 155 | } 156 | }, { 157 | key: 'epfd', 158 | get: function get() { 159 | return this._bitField.getInt(270, 4, true); 160 | } 161 | }, { 162 | key: 'etaMonth', 163 | get: function get() { 164 | return this._bitField.getInt(274, 4, true) || NaN; 165 | } 166 | }, { 167 | key: 'etaDay', 168 | get: function get() { 169 | return this._bitField.getInt(278, 5, true) || NaN; 170 | } 171 | }, { 172 | key: 'etaHour', 173 | get: function get() { 174 | return this._bitField.getInt(283, 5, true) || NaN; 175 | } 176 | }, { 177 | key: 'etaMinute', 178 | get: function get() { 179 | return this._bitField.getInt(288, 6, true) || NaN; 180 | } 181 | }, { 182 | key: 'draught', 183 | get: function get() { 184 | return this._bitField.getInt(294, 8, true) / 10; 185 | } 186 | }, { 187 | key: 'destination', 188 | get: function get() { 189 | return this._formatStr(this._bitField.getString(302, 120)); 190 | } 191 | }]); 192 | 193 | return Ais05Msg; 194 | }(_AisMessage3.default); 195 | 196 | exports.default = Ais05Msg; 197 | -------------------------------------------------------------------------------- /lib/Ais18Msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _AisBitField = require('./AisBitField'); 10 | 11 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 12 | 13 | var _AisMessage2 = require('./AisMessage'); 14 | 15 | var _AisMessage3 = _interopRequireDefault(_AisMessage2); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | /* 26 | * AisParser: A parser for NMEA0183 AIS messages. 27 | * Copyright (C) 2017 Thomas Runte . 28 | * 29 | * This program is free software: you can redistribute it and/or modify 30 | * it under the terms of the Apache License Version 2.0 as published by 31 | * Apache Software foundation. 32 | * 33 | * This program is distributed in the hope that it will be useful, 34 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 | * GNU General Public License for more details. 37 | * 38 | * You should have received a copy of the Apache License Version 2.0 39 | * along with this program. If not, see . 40 | */ 41 | 42 | var MOD_NAME = 'Ais18Msg'; 43 | 44 | /* 45 | |============================================================================== 46 | |Field |Len |Description |Member |T|Units 47 | |0-5 | 6 |Message Type |type |u|Constant: 18 48 | |6-7 | 2 |Repeat Indicator |repeat |u|As in Common Navigation Block 49 | |8-37 |30 |MMSI |mmsi |u|9 decimal digits 50 | |38-45 | 8 |Regional Reserved |reserved |x|Not used 51 | |46-55 |10 |Speed Over Ground |speed |u|As in common navigation block 52 | |56-56 | 1 |Position Accuracy |accuracy |b|See below 53 | |57-84 |28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 54 | |85-111 |27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 55 | |112-123 |12 |Course Over Ground |course |U1|0.1 degrees from true north 56 | |124-132 | 9 |True Heading |heading |u|0 to 359 degrees, 511 = N/A 57 | |133-138 | 6 |Time Stamp |second |u|Second of UTC timestamp. 58 | TODO: 59 | |139-140 | 2 |Regional reserved |regional |u|Uninterpreted 60 | |141-141 | 1 |CS Unit |cs |b|0=Class B SOTDMA unit 61 | 1=Class B CS (Carrier Sense) unit 62 | |142-142 | 1 |Display flag |display |b|0=No visual display, 63 | 1=Has display, 64 | (Probably not reliable). 65 | |143-143 | 1 |DSC Flag |dsc |b|If 1, unit is attached to a VHF 66 | voice radio with DSC capability. 67 | |144-144 | 1 |Band flag |band |b|Base stations can command units 68 | to switch frequency. If this flag 69 | is 1, the unit can use any part 70 | of the marine channel. 71 | |145-145 | 1 |Message 22 flag |msg22 |b|If 1, unit can accept a channel 72 | assignment via Message Type 22. 73 | |146-146 | 1 |Assigned |assigned |b|Assigned-mode flag: 74 | 0 = autonomous mode (default), 75 | 1 = assigned mode. 76 | |147-147 | 1 |RAIM flag |raim |b|As for common navigation block 77 | |148-167 |20 |Radio status |radio |u|See <> for details. 78 | |============================================================================== 79 | */ 80 | 81 | var SUPPORTED_FIELDS = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'heading', 'sogStatus', 'sog', 'cog', 'latitude', 'longitude', 'posAccuracy', 'utcTsSec', 'utcTsStatus']; 82 | var suppValuesValid = false; 83 | var suppValues = {}; 84 | 85 | var Ais18Msg = function (_AisMessage) { 86 | _inherits(Ais18Msg, _AisMessage); 87 | 88 | function Ais18Msg(aisType, bitField, channel) { 89 | _classCallCheck(this, Ais18Msg); 90 | 91 | var _this = _possibleConstructorReturn(this, (Ais18Msg.__proto__ || Object.getPrototypeOf(Ais18Msg)).call(this, aisType, bitField, channel)); 92 | 93 | if (bitField.bits >= 167) { 94 | _this._valid = 'VALID'; 95 | } else { 96 | _this._valid = 'INVALID'; 97 | _this._errMsg = 'invalid bitcount for type 18 msg:' + bitField.bits; 98 | } 99 | return _this; 100 | } 101 | 102 | _createClass(Ais18Msg, [{ 103 | key: '_getRawHeading', 104 | value: function _getRawHeading() { 105 | return this._bitField.getInt(124, 9, true); 106 | } 107 | }, { 108 | key: '_getRawSog', 109 | value: function _getRawSog() { 110 | return this._bitField.getInt(46, 10, true); 111 | } 112 | }, { 113 | key: '_getRawCog', 114 | value: function _getRawCog() { 115 | return this._bitField.getInt(112, 12, true); 116 | } 117 | }, { 118 | key: '_getUtcSec', 119 | value: function _getUtcSec() { 120 | return this._bitField.getInt(133, 6, true); 121 | } 122 | }, { 123 | key: '_getRawLat', 124 | value: function _getRawLat() { 125 | return this._bitField.getInt(85, 27, false); 126 | } 127 | }, { 128 | key: '_getRawLon', 129 | value: function _getRawLon() { 130 | return this._bitField.getInt(57, 28, false); 131 | } 132 | }, { 133 | key: 'supportedValues', 134 | get: function get() { 135 | if (!suppValuesValid) { 136 | SUPPORTED_FIELDS.forEach(function (field) { 137 | var unit = _AisMessage3.default.getUnit(field); 138 | if (unit) { 139 | suppValues[field] = unit; 140 | } else { 141 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 142 | } 143 | }); 144 | suppValuesValid = true; 145 | } 146 | return suppValues; 147 | } 148 | }, { 149 | key: 'class', 150 | get: function get() { 151 | return 'B'; 152 | } 153 | }, { 154 | key: 'posAccuracy', 155 | get: function get() { 156 | return this._bitField.getInt(56, 1, true) === 1; 157 | } 158 | }]); 159 | 160 | return Ais18Msg; 161 | }(_AisMessage3.default); 162 | 163 | exports.default = Ais18Msg; 164 | -------------------------------------------------------------------------------- /lib/Ais19Msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _AisBitField = require('./AisBitField'); 10 | 11 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 12 | 13 | var _AisMessage2 = require('./AisMessage'); 14 | 15 | var _AisMessage3 = _interopRequireDefault(_AisMessage2); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | /* 26 | * AisParser: A parser for NMEA0183 AIS messages. 27 | * Copyright (C) 2017 Thomas Runte . 28 | * 29 | * This program is free software: you can redistribute it and/or modify 30 | * it under the terms of the Apache License Version 2.0 as published by 31 | * Apache Software foundation. 32 | * 33 | * This program is distributed in the hope that it will be useful, 34 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 | * GNU General Public License for more details. 37 | * 38 | * You should have received a copy of the Apache License Version 2.0 39 | * along with this program. If not, see . 40 | */ 41 | 42 | var MOD_NAME = 'Ais21Msg'; 43 | /* 44 | |============================================================================== 45 | |Field |Len |Description |Member |T|Units 46 | |0-5 | 6 |Message Type |type |u|Constant: 19 47 | |6-7 | 2 |Repeat Indicator |repeat |u|As in CNN 48 | |8-37 | 30 |MMSI |mmsi |u|9 digits 49 | |38-45 | 8 |Regional Reserved |reserved |u| 50 | |46-55 | 10 |Speed Over Ground |speed |u|As in CNB. 51 | |56-56 | 1 |Position Accuracy |accuracy |b|As in CNB. 52 | |57-84 | 28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 53 | |85-111 | 27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 54 | |112-123 | 12 |Course Over Ground |course |U1|Relative to true north, 55 | units of 0.1 degrees 56 | |124-132 | 9 |True Heading |heading |u|0 to 359 degrees, 57 | 511 = N/A 58 | |133-138 | 6 |Time Stamp |second |u|Second of UTC timestamp. 59 | |139-142 | 4 |Regional reserved |regional |u|Uninterpreted 60 | |143-262 |120 |Name |shipname |s|20 6-bit characters 61 | |263-270 | 8 |Type of ship and cargo |shiptype |u|As in Message 5 62 | |271-279 | 9 |Dimension to Bow |to_bow |u|Meters 63 | |280-288 | 9 |Dimension to Stern |to_stern |u|Meters 64 | |289-294 | 6 |Dimension to Port |to_port |u|Meters 65 | |295-300 | 6 |Dimension to Starboard |to_starboard |u|Meters 66 | |301-304 | 4 |Position Fix Type |epfd |e|See "EPFD Fix Types" 67 | |305-305 | 1 |RAIM flag |raim |b|As in CNB. 68 | |306-306 | 1 |DTE |dte |b|0=Data terminal ready, 69 | 1=Not ready (default). 70 | |307-307 | 1 |Assigned mode flag |assigned |u|See <> for details 71 | |308-311 | 4 |Spare | |x|Unused, should be zero 72 | |============================================================================== 73 | */ 74 | var SUPPORTED_FIELDS = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'class', 'heading', 'sogStatus', 'sog', 'cog', 'latitude', 'longitude', 'posAccuracy', 'utcTsSec', 'utcTsStatus', 'name', 'shipType', 'shipTypeStr', 'dimToBow', 'dimToBowStatus', 'dimToStern', 'dimToSternStatus', 'dimToPort', 'dimToPortStatus', 'dimToStbrd', 'dimToStbrdStatus', 'epfd', 'epfdStr']; 75 | 76 | var suppValuesValid = false; 77 | var suppValues = {}; 78 | 79 | var Ais19Msg = function (_AisMessage) { 80 | _inherits(Ais19Msg, _AisMessage); 81 | 82 | function Ais19Msg(aisType, bitField, channel) { 83 | _classCallCheck(this, Ais19Msg); 84 | 85 | var _this = _possibleConstructorReturn(this, (Ais19Msg.__proto__ || Object.getPrototypeOf(Ais19Msg)).call(this, aisType, bitField, channel)); 86 | 87 | if (bitField.bits >= 311) { 88 | _this._valid = 'VALID'; 89 | } else { 90 | _this._valid = 'INVALID'; 91 | _this._errMsg = 'invalid bitcount for type 19 msg:' + bitField.bits; 92 | } 93 | return _this; 94 | } 95 | 96 | _createClass(Ais19Msg, [{ 97 | key: '_getRawHeading', 98 | 99 | 100 | // |124-132 | 9 |True Heading |heading |u|0 to 359 degrees, 101 | value: function _getRawHeading() { 102 | return this._bitField.getInt(124, 9, true); 103 | } 104 | 105 | //|46-55 | 10 |Speed Over Ground |speed |u|As in CNB. 106 | 107 | }, { 108 | key: '_getRawSog', 109 | value: function _getRawSog() { 110 | return this._bitField.getInt(46, 10, true); 111 | } 112 | 113 | // |112-123 | 12 |Course Over Ground |course |U1|Relative to true north, 114 | 115 | }, { 116 | key: '_getRawCog', 117 | value: function _getRawCog() { 118 | return this._bitField.getInt(112, 12, true); 119 | } 120 | 121 | // |56-56 | 1 |Position Accuracy |accuracy |b|As in CNB. 122 | 123 | }, { 124 | key: '_getUtcSec', 125 | 126 | 127 | // |133-138 | 6 |Time Stamp |second |u|Second of UTC timestamp. 128 | value: function _getUtcSec() { 129 | return this._bitField.getInt(133, 6, true); 130 | } 131 | 132 | // |85-111 | 27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 133 | 134 | }, { 135 | key: '_getRawLat', 136 | value: function _getRawLat() { 137 | return this._bitField.getInt(85, 27, false); 138 | } 139 | 140 | // |57-84 | 28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 141 | 142 | }, { 143 | key: '_getRawLon', 144 | value: function _getRawLon() { 145 | return this._bitField.getInt(57, 28, false); 146 | } 147 | 148 | // |143-262 |120 |Name |shipname |s|20 6-bit characters 149 | 150 | }, { 151 | key: '_getDimToBow', 152 | 153 | 154 | // |271-279 | 9 |Dimension to Bow |to_bow |u|Meters 155 | value: function _getDimToBow() { 156 | return this._bitField.getInt(271, 9, true); 157 | } 158 | 159 | // |280-288 | 9 |Dimension to Stern |to_stern |u|Meters 160 | 161 | }, { 162 | key: '_getDimToStern', 163 | value: function _getDimToStern() { 164 | return this._bitField.getInt(280, 9, true); 165 | } 166 | 167 | // |289-294 | 6 |Dimension to Port |to_port |u|Meters 168 | 169 | }, { 170 | key: '_getDimToPort', 171 | value: function _getDimToPort() { 172 | return this._bitField.getInt(289, 6, true); 173 | } 174 | 175 | // |295-300 | 6 |Dimension to Starboard |to_starboard |u|Meters 176 | 177 | }, { 178 | key: '_getDimToStbrd', 179 | value: function _getDimToStbrd() { 180 | return this._bitField.getInt(295, 6, true); 181 | } 182 | 183 | // |301-304 | 4 |Position Fix Type |epfd |e|See "EPFD Fix Types" 184 | 185 | }, { 186 | key: 'supportedValues', 187 | get: function get() { 188 | if (!suppValuesValid) { 189 | SUPPORTED_FIELDS.forEach(function (field) { 190 | var unit = _AisMessage3.default.getUnit(field); 191 | if (unit) { 192 | suppValues[field] = unit; 193 | } else { 194 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 195 | } 196 | }); 197 | suppValuesValid = true; 198 | } 199 | return suppValues; 200 | } 201 | }, { 202 | key: 'class', 203 | get: function get() { 204 | return 'B'; 205 | } 206 | }, { 207 | key: 'posAccuracy', 208 | get: function get() { 209 | return this._bitField.getInt(56, 1, true) === 1; 210 | } 211 | }, { 212 | key: 'name', 213 | get: function get() { 214 | return this._formatStr(this._bitField.getString(143, 120)); 215 | } 216 | 217 | // |263-270 | 8 |Type of ship and cargo |shiptype |u|As in Message 5 218 | 219 | }, { 220 | key: 'shipType', 221 | get: function get() { 222 | return this._bitField.getInt(263, 8, true); 223 | } 224 | }, { 225 | key: 'epfd', 226 | get: function get() { 227 | return this._bitField.getInt(301, 4, true); 228 | } 229 | }]); 230 | 231 | return Ais19Msg; 232 | }(_AisMessage3.default); 233 | 234 | exports.default = Ais19Msg; 235 | -------------------------------------------------------------------------------- /lib/Ais21Msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _AisBitField = require('./AisBitField'); 10 | 11 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 12 | 13 | var _AisMessage2 = require('./AisMessage'); 14 | 15 | var _AisMessage3 = _interopRequireDefault(_AisMessage2); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | /* 26 | * AisParser: A parser for NMEA0183 AIS messages. 27 | * Copyright (C) 2017 Thomas Runte . 28 | * 29 | * This program is free software: you can redistribute it and/or modify 30 | * it under the terms of the Apache License Version 2.0 as published by 31 | * Apache Software foundation. 32 | * 33 | * This program is distributed in the hope that it will be useful, 34 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 | * GNU General Public License for more details. 37 | * 38 | * You should have received a copy of the Apache License Version 2.0 39 | * along with this program. If not, see . 40 | */ 41 | 42 | var MOD_NAME = 'Ais21Msg'; 43 | /* 44 | |============================================================================== 45 | |Field |Len |Description |Member |T|Units 46 | |0-5 | 6 |Message Type |type |u|Constant: 21 47 | |6-7 | 2 |Repeat Indicator |repeat |u|As in CNB 48 | |8-37 |30 |MMSI |mmsi |u|9 digits 49 | |38-42 | 5 |Aid type |aid_type |e|See "Navaid Types" 50 | |43-162 1|120 |Name |name |t|Name in sixbit chars 51 | |163-163 | 1 |Position Accuracy |accuracy |b|As in CNB 52 | |164-191 |28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 53 | |192-218 |27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 54 | |219-227 | 9 |Dimension to Bow |to_bow |u|Meters 55 | |228-236 | 9 |Dimension to Stern |to_stern |u|Meters 56 | |237-242 | 6 |Dimension to Port |to_port |u|Meters 57 | |243-248 | 6 |Dimension to Starboard |to_starboard|u|Meters 58 | |249-252 | 4 |Type of EPFD |epfd |e|As in Message Type 4 59 | |253-258 | 6 |UTC Second |second |u|As in Message Type 5 60 | |259-259 | 1 |Off-Position Indicator |off_position|b|See Below 61 | |260-267 | 8 |Regional reserved |regional |u|Uninterpreted 62 | |268-268 | 1 |RAIM flag |raim |b|As in CNB 63 | |269-269 | 1 |Virtual-aid flag |virtual_aid |b|See Below 64 | |270-270 | 1 |Assigned-mode flag |assigned |b|See <> for details 65 | |271-271 | 1 |Spare | |x|Not used 66 | |272-360 |88 |Name Extension | |t|See Below 67 | |============================================================================== 68 | */ 69 | 70 | var SUPPORTED_FIELDS = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'name', 'latitude', 'longitude', 'posAccuracy', 'dimToBow', 'dimToBowStatus', 'dimToStern', 'dimToSternStatus', 'dimToPort', 'dimToPortStatus', 'dimToStbrd', 'dimToStbrdStatus', 'length', 'width', 'epfd', 'epfdStr', 'utcTsSec', 'utcTsStatus', 'offPosInd', 'aidType', 'aidTypeStr', 'nameExt']; 71 | 72 | var suppValuesValid = false; 73 | var suppValues = {}; 74 | 75 | var Ais21Msg = function (_AisMessage) { 76 | _inherits(Ais21Msg, _AisMessage); 77 | 78 | function Ais21Msg(aisType, bitField, channel) { 79 | _classCallCheck(this, Ais21Msg); 80 | 81 | var _this = _possibleConstructorReturn(this, (Ais21Msg.__proto__ || Object.getPrototypeOf(Ais21Msg)).call(this, aisType, bitField, channel)); 82 | 83 | if (bitField.bits >= 271) { 84 | _this._valid = 'VALID'; 85 | } else { 86 | _this._valid = 'INVALID'; 87 | _this._errMsg = 'invalid bitcount for type 21 msg:' + bitField.bits; 88 | } 89 | return _this; 90 | } 91 | 92 | _createClass(Ais21Msg, [{ 93 | key: '_getRawLon', 94 | 95 | 96 | // |164-191 |28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 97 | value: function _getRawLon() { 98 | return this._bitField.getInt(164, 28, false); 99 | } 100 | 101 | //|192-218 |27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 102 | 103 | }, { 104 | key: '_getRawLat', 105 | value: function _getRawLat() { 106 | return this._bitField.getInt(192, 27, false); 107 | } 108 | 109 | // |219-227 | 9 |Dimension to Bow |to_bow |u|Meters 110 | 111 | }, { 112 | key: '_getDimToBow', 113 | value: function _getDimToBow() { 114 | return this._bitField.getInt(219, 9, true); 115 | } 116 | 117 | // |228-236 | 9 |Dimension to Stern |to_stern |u|Meters 118 | 119 | }, { 120 | key: '_getDimToStern', 121 | value: function _getDimToStern() { 122 | return this._bitField.getInt(228, 9, true); 123 | } 124 | 125 | // |237-242 | 6 |Dimension to Port |to_port |u|Meters 126 | 127 | }, { 128 | key: '_getDimToPort', 129 | value: function _getDimToPort() { 130 | return this._bitField.getInt(237, 6, true); 131 | } 132 | 133 | // |243-248 | 6 |Dimension to Starboard |to_starboard|u|Meters 134 | 135 | }, { 136 | key: '_getDimToStbrd', 137 | value: function _getDimToStbrd() { 138 | return this._bitField.getInt(243, 6, true); 139 | } 140 | 141 | // |249-252 | 4 |Type of EPFD |epfd |e|As in Message Type 4 142 | 143 | }, { 144 | key: '_getUtcSec', 145 | 146 | 147 | // |253-258 | 6 |UTC Second |second |u|As in Message Type 5 148 | value: function _getUtcSec() { 149 | return this._bitField.getInt(253, 6, true); 150 | } 151 | 152 | // |259-259 | 1 |Off-Position Indicator |off_position|b|See Below 153 | 154 | }, { 155 | key: 'supportedValues', 156 | get: function get() { 157 | if (!suppValuesValid) { 158 | SUPPORTED_FIELDS.forEach(function (field) { 159 | var unit = _AisMessage3.default.getUnit(field); 160 | if (unit) { 161 | suppValues[field] = unit; 162 | } else { 163 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 164 | } 165 | }); 166 | suppValuesValid = true; 167 | } 168 | return suppValues; 169 | } 170 | 171 | // |38-42 | 5 |Aid type |aid_type |e|See "Navaid Types" 172 | 173 | }, { 174 | key: 'aidType', 175 | get: function get() { 176 | return this._bitField.getInt(38, 5, true); 177 | } 178 | 179 | // |43-162 1|120 |Name |name |t|Name in sixbit chars 180 | 181 | }, { 182 | key: 'name', 183 | get: function get() { 184 | return this._formatStr(this._bitField.getString(43, 162)); 185 | } 186 | 187 | // |163-163 | 1 |Position Accuracy |accuracy |b|As in CNB 188 | 189 | }, { 190 | key: 'posAccuracy', 191 | get: function get() { 192 | return this._bitField.getInt(163, 1, true) === 1; 193 | } 194 | }, { 195 | key: 'epfd', 196 | get: function get() { 197 | return this._bitField.getInt(249, 4, true); 198 | } 199 | }, { 200 | key: 'offPosInd', 201 | get: function get() { 202 | if (this._getUtcSec() < 60) { 203 | return this._bitField.getInt(163, 1, true) === 0 ? 'IN_POS' : 'OFF_POS'; 204 | } else { 205 | return 'NA'; 206 | } 207 | } 208 | 209 | // |272-360 |88 |Name Extension | |t|See Below 210 | 211 | }, { 212 | key: 'nameExt', 213 | get: function get() { 214 | if (this._bitField.bits > 272) { 215 | var chars = Math.floor((this._bitField.bits - 272) / 6); 216 | if (chars > 0) { 217 | return this._formatStr(this._bitField.getString(272, chars * 6)); 218 | } 219 | } 220 | return ''; 221 | } 222 | }]); 223 | 224 | return Ais21Msg; 225 | }(_AisMessage3.default); 226 | 227 | exports.default = Ais21Msg; 228 | -------------------------------------------------------------------------------- /lib/Ais24Msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _AisBitField = require('./AisBitField'); 10 | 11 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 12 | 13 | var _AisMessage2 = require('./AisMessage'); 14 | 15 | var _AisMessage3 = _interopRequireDefault(_AisMessage2); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | /* 26 | * AisParser: A parser for NMEA0183 AIS messages. 27 | * Copyright (C) 2017 Thomas Runte . 28 | * 29 | * This program is free software: you can redistribute it and/or modify 30 | * it under the terms of the Apache License Version 2.0 as published by 31 | * Apache Software foundation. 32 | * 33 | * This program is distributed in the hope that it will be useful, 34 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 | * GNU General Public License for more details. 37 | * 38 | * You should have received a copy of the Apache License Version 2.0 39 | * along with this program. If not, see . 40 | */ 41 | 42 | var MOD_NAME = 'Ais24Msg'; 43 | 44 | var SUPPORTED_FIELDS_A = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'partNo', 'name']; 45 | 46 | var SUPPORTED_FIELDS_B_NO_TENDER = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'partNo', 'shipType', 'callSign', 'vendorId', 'dimToBow', 'dimToBowStatus', 'dimToStern', 'dimToSternStatus', 'dimToPort', 'dimToPortStatus', 'dimToStbrd', 'dimToStbrdStatus']; 47 | 48 | var SUPPORTED_FIELDS_B_TENDER = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'partNo', 'shipType', 'callSign', 'vendorId', 'mothershipMmsi']; 49 | 50 | var suppValuesValidA = false; 51 | var suppValuesA = {}; 52 | var suppValuesValidBNT = false; 53 | var suppValuesBT = {}; 54 | var suppValuesValidBT = false; 55 | var suppValuesBNT = {}; 56 | 57 | /* 58 | |============================================================================== 59 | |Field |Len |Description | Member |T|Units 60 | |0-5 | 6 | Message Type | type |u|Constant: 24 61 | |6-7 | 2 | Repeat Indicator | repeat |u|As in CNB 62 | |8-37 | 30 | MMSI | mmsi |u|9 digits 63 | |38-39 | 2 | Part Number | partno |u|0-1 64 | |40-159 |120 | Vessel Name | shipname |t|(Part A) 20 sixbit chars 65 | |160-167 | 8 | Spare | |x|(Part A) Not used 66 | |40-47 | 8 | Ship Type | shiptype |e|(Part B) See "Ship Types" 67 | |48-89 | 42 | Vendor ID | vendorid |t|(Part B) 7 six-bit chars 68 | |90-131 | 42 | Call Sign | callsign |t|(Part B) As in Message Type 5 69 | |132-140 | 9 | Dimension to Bow | to_bow |u|(Part B) Meters 70 | |141-149 | 9 | Dimension to Stern | to_stern |u|(Part B) Meters 71 | |150-155 | 6 | Dimension to Port | to_port |u|(Part B) Meters 72 | |156-161 | 6 | Dimension to Starboard| to_starboard |u|(Part B) Meters 73 | |132-161 | 30 | Mothership MMSI | mothership_mmsi|u|(Part B) See below 74 | |162-167 | 6 | Spare | |x|(Part B) Not used 75 | |=============================================================================== 76 | */ 77 | 78 | var Ais24Msg = function (_AisMessage) { 79 | _inherits(Ais24Msg, _AisMessage); 80 | 81 | function Ais24Msg(aisType, bitField, channel) { 82 | _classCallCheck(this, Ais24Msg); 83 | 84 | var _this = _possibleConstructorReturn(this, (Ais24Msg.__proto__ || Object.getPrototypeOf(Ais24Msg)).call(this, aisType, bitField, channel)); 85 | 86 | if (bitField.bits >= 39) { 87 | _this._partNo = _this._bitField.getInt(38, 2, true) ? 1 : 0; 88 | 89 | if (_this._partNo === 0 && bitField.bits >= 159 || bitField.bits >= 161) { 90 | _this._valid = 'VALID'; 91 | return _possibleConstructorReturn(_this); 92 | } 93 | } 94 | _this._valid = 'INVALID'; 95 | _this._errMsg = 'invalid bitcount for type 24 msg:' + bitField.bits; 96 | return _this; 97 | } 98 | 99 | _createClass(Ais24Msg, [{ 100 | key: '_isTender', 101 | value: function _isTender() { 102 | if (typeof this._tender !== 'boolean') { 103 | this._tender = String(this.mmsi).startsWith('98'); 104 | } 105 | return this._tender; 106 | } 107 | }, { 108 | key: '_getDimToBow', 109 | value: function _getDimToBow() { 110 | if (this.partNo === 1 && !this._isTender()) { 111 | return this._bitField.getInt(132, 9, true); 112 | } else { 113 | return NaN; 114 | } 115 | } 116 | }, { 117 | key: '_getDimToStern', 118 | value: function _getDimToStern() { 119 | if (this.partNo === 1 && !this._isTender()) { 120 | return this._bitField.getInt(141, 9, true); 121 | } else { 122 | return NaN; 123 | } 124 | } 125 | }, { 126 | key: '_getDimToPort', 127 | value: function _getDimToPort() { 128 | if (this.partNo === 1 && !this._isTender()) { 129 | return this._bitField.getInt(150, 6, true); 130 | } else { 131 | return NaN; 132 | } 133 | } 134 | }, { 135 | key: '_getDimToStbrd', 136 | value: function _getDimToStbrd() { 137 | if (this.partNo === 1 && !this._isTender()) { 138 | return this._bitField.getInt(156, 6, true); 139 | } else { 140 | return NaN; 141 | } 142 | } 143 | }, { 144 | key: 'supportedValues', 145 | get: function get() { 146 | if (this.partNo === 0) { 147 | if (!suppValuesValidA) { 148 | SUPPORTED_FIELDS_A.forEach(function (field) { 149 | var unit = _AisMessage3.default.getUnit(field); 150 | if (unit) { 151 | suppValuesA[field] = unit; 152 | } else { 153 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 154 | } 155 | }); 156 | suppValuesValidA = true; 157 | } 158 | return suppValuesA; 159 | } else { 160 | if (this._isTender()) { 161 | if (!suppValuesValidBT) { 162 | SUPPORTED_FIELDS_B_TENDER.forEach(function (field) { 163 | var unit = _AisMessage3.default.getUnit(field); 164 | if (unit) { 165 | suppValuesBT[field] = unit; 166 | } else { 167 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 168 | } 169 | }); 170 | suppValuesValidBT = true; 171 | } 172 | return suppValuesBT; 173 | } else { 174 | if (!suppValuesValidBNT) { 175 | SUPPORTED_FIELDS_B_NO_TENDER.forEach(function (field) { 176 | var unit = _AisMessage3.default.getUnit(field); 177 | if (unit) { 178 | suppValuesBNT[field] = unit; 179 | } else { 180 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 181 | } 182 | }); 183 | suppValuesValidBNT = true; 184 | } 185 | return suppValuesBNT; 186 | } 187 | } 188 | } 189 | }, { 190 | key: 'partNo', 191 | get: function get() { 192 | if (typeof this._partNo === 'number') { 193 | return this._partNo; 194 | } else { 195 | return this._partNo = this._bitField.getInt(38, 2, true) ? 1 : 0; 196 | } 197 | } 198 | }, { 199 | key: 'name', 200 | get: function get() { 201 | if (this.partNo === 0) { 202 | return this._formatStr(this._bitField.getString(40, 120)); 203 | } else { 204 | return ''; 205 | } 206 | } 207 | }, { 208 | key: 'shipType', 209 | get: function get() { 210 | if (this.partNo === 1) { 211 | return this._bitField.getInt(40, 8, true); 212 | } else { 213 | return NaN; 214 | } 215 | } 216 | }, { 217 | key: 'callSign', 218 | get: function get() { 219 | if (this.partNo === 1) { 220 | return this._formatStr(this._bitField.getString(90, 42)); 221 | } else { 222 | return ''; 223 | } 224 | } 225 | }, { 226 | key: 'vendorId', 227 | get: function get() { 228 | if (this.partNo === 1) { 229 | return this._formatStr(this._bitField.getString(48, 42)); 230 | } else { 231 | return ''; 232 | } 233 | } 234 | }, { 235 | key: 'mothershipMmsi', 236 | get: function get() { 237 | if (this.partNo === 1 && this._isTender()) { 238 | return this._bitField.getInt(132, 30, true); 239 | } else { 240 | return NaN; 241 | } 242 | } 243 | }]); 244 | 245 | return Ais24Msg; 246 | }(_AisMessage3.default); 247 | 248 | exports.default = Ais24Msg; 249 | -------------------------------------------------------------------------------- /lib/Ais27Msg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _AisBitField = require('./AisBitField'); 10 | 11 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 12 | 13 | var _AisMessage2 = require('./AisMessage'); 14 | 15 | var _AisMessage3 = _interopRequireDefault(_AisMessage2); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | /* 26 | * AisParser: A parser for NMEA0183 AIS messages. 27 | * Copyright (C) 2017 Thomas Runte . 28 | * 29 | * This program is free software: you can redistribute it and/or modify 30 | * it under the terms of the Apache License Version 2.0 as published by 31 | * Apache Software foundation. 32 | * 33 | * This program is distributed in the hope that it will be useful, 34 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 | * GNU General Public License for more details. 37 | * 38 | * You should have received a copy of the Apache License Version 2.0 39 | * along with this program. If not, see . 40 | */ 41 | 42 | var MOD_NAME = 'Ais27Msg'; 43 | 44 | var SUPPORTED_FIELDS = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'class', 'navStatus', 'navStatusStr', 'sogStatus', 'sog', 'cog', 'latitude', 'longitude', 'posAccuracy']; 45 | 46 | var suppValuesValid = false; 47 | var suppValues = {}; 48 | 49 | /* 50 | |============================================================================== 51 | |Field |Len |Description |Member |T|Units 52 | |0-5 | 6 |Message Type |type |u|Constant: 27 53 | |6-7 | 2 |Repeat Indicator |repeat |u|Message repeat count 54 | |8-37 |30 |MMSI |mmsi |u|9 decimal digits 55 | |38-38 | 1 |Position Accuracy |accuracy |u| 56 | |39-39 | 1 |RAIM flag |raim |u| 57 | |40-43 | 4 |Navigation Status |status |e|See "Navigation Status" 58 | |44-61 |18 |Longitude |lon |I4|minutes/10 East positive, West negative 181000 = N/A 59 | |62-78 |17 |Latitude |lat |I4|minutes/10 North positive, South negative 91000 = N/A 60 | |79-84 |6 |Speed Over Ground (SOG) |speed |u|Knots (0-62); 63 = N/A 61 | |85-93 |9 |Course Over Ground (COG)|course |u|0 to 359 degrees, 511 = not available. 62 | |94-94 |1 |GNSS Position status |gnss |u|0 = current GNSS position 1 = not GNSS position (default) 63 | |95-95 |1 |spare | |u|not used 64 | |============================================================================== 65 | 66 | */ 67 | 68 | var Ais27Msg = function (_AisMessage) { 69 | _inherits(Ais27Msg, _AisMessage); 70 | 71 | function Ais27Msg(aisType, bitField, channel) { 72 | _classCallCheck(this, Ais27Msg); 73 | 74 | var _this = _possibleConstructorReturn(this, (Ais27Msg.__proto__ || Object.getPrototypeOf(Ais27Msg)).call(this, aisType, bitField, channel)); 75 | 76 | if (bitField.bits >= 94) { 77 | _this._valid = 'VALID'; 78 | } else { 79 | _this._valid = 'INVALID'; 80 | _this._errMsg = 'invalid bitcount for type CNB msg:' + bitField.bits; 81 | } 82 | return _this; 83 | } 84 | 85 | _createClass(Ais27Msg, [{ 86 | key: '_getRawSog', 87 | value: function _getRawSog() { 88 | return this._bitField.getInt(79, 6, true) * 10; 89 | } 90 | }, { 91 | key: '_getRawCog', 92 | value: function _getRawCog() { 93 | return this._bitField.getInt(85, 9, true) * 10; 94 | } 95 | }, { 96 | key: '_getRawLat', 97 | value: function _getRawLat() { 98 | return this._bitField.getInt(62, 17, false) * 1000; 99 | } 100 | }, { 101 | key: '_getRawLon', 102 | value: function _getRawLon() { 103 | return this._bitField.getInt(44, 18, false) * 1000; 104 | } 105 | }, { 106 | key: 'class', 107 | get: function get() { 108 | return 'A'; 109 | } 110 | }, { 111 | key: 'supportedValues', 112 | get: function get() { 113 | if (!suppValuesValid) { 114 | SUPPORTED_FIELDS.forEach(function (field) { 115 | var unit = _AisMessage3.default.getUnit(field); 116 | if (unit) { 117 | suppValues[field] = unit; 118 | } else { 119 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 120 | } 121 | }); 122 | suppValuesValid = true; 123 | } 124 | return suppValues; 125 | } 126 | }, { 127 | key: 'navStatus', 128 | get: function get() { 129 | return this._bitField.getInt(40, 4, true); 130 | } 131 | }, { 132 | key: 'posAccuracy', 133 | get: function get() { 134 | return this._bitField.getInt(38, 1, true); 135 | } 136 | }]); 137 | 138 | return Ais27Msg; 139 | }(_AisMessage3.default); 140 | 141 | exports.default = Ais27Msg; 142 | -------------------------------------------------------------------------------- /lib/AisBitField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | /* 12 | * AisParser: A parser for NMEA0183 AIS messages. 13 | * Copyright (C) 2017 Thomas Runte . 14 | * 15 | * This program is free software: you can redistribute it and/or modify 16 | * it under the terms of the Apache License Version 2.0 as published by 17 | * Apache Software foundation. 18 | * 19 | * This program is distributed in the hope that it will be useful, 20 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | * GNU General Public License for more details. 23 | * 24 | * You should have received a copy of the Apache License Version 2.0 25 | * along with this program. If not, see . 26 | */ 27 | 28 | var DEBUG = false; 29 | var MOD_NAME = 'AisBitField'; 30 | var AIS_CHR_TBL = ['@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', ' ', '!', '\'', '#', '$', '%', '&', '"', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?']; 31 | 32 | var padStart = function padStart(str, tgtLen) { 33 | var padStr = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '0'; 34 | 35 | var fillLen = tgtLen - str.length; 36 | if (fillLen <= 0) { 37 | return str; 38 | } 39 | 40 | var filler = padStr; 41 | var fLen = filler.length; 42 | while (fLen < fillLen) { 43 | var rem = fillLen - fLen; 44 | filler += fLen > rem ? filler.slice(0, rem) : filler; 45 | fLen = filler.length; 46 | } 47 | if (fLen > fillLen) { 48 | filler = filler.slice(0, fillLen); 49 | } 50 | return filler + str; 51 | }; 52 | 53 | var printByte = function printByte(byte) { 54 | var bits = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 8; 55 | 56 | return Number(byte & (1 << bits) - 1).toString(2).padStart(bits, '0'); 57 | }; 58 | 59 | var AisBitField = function () { 60 | function AisBitField(str, padBits) { 61 | _classCallCheck(this, AisBitField); 62 | 63 | if (DEBUG) console.log(MOD_NAME + '.constructor(' + str + ',' + padBits + ')'); 64 | if (str) { 65 | this._aisStr = str; 66 | var strLen = this._strLen = str.length; 67 | this._bits = strLen * 6; 68 | var len = Math.floor(this._bits / 8) + (this._bits % 8 == 0 ? 0 : 1); 69 | this._bytes = new Array(len); 70 | if (DEBUG) console.log(MOD_NAME + '.constructor() array size:' + this._bytes.length); 71 | if (this._bits > padBits) { 72 | this._bits -= padBits; 73 | } else { 74 | throw MOD_NAME + ".parse() invalid bitcount encountered:" + (this._bits - padBits); 75 | } 76 | } else { 77 | this._bits = 0; 78 | } 79 | } 80 | 81 | _createClass(AisBitField, [{ 82 | key: '_getByte', 83 | 84 | 85 | // return 6-bit 86 | value: function _getByte(idx) { 87 | var byte = this._bytes[idx]; 88 | if (typeof byte === 'number') { 89 | return byte; 90 | } else { 91 | var char = this._aisStr.charCodeAt(idx); 92 | if (char > 47) { 93 | if (char < 88) { 94 | return this._bytes[idx] = char - 48; 95 | } else { 96 | if (char < 96) { 97 | throw MOD_NAME + '.parse() invalid character encountered:' + char + ' at index ' + idx; 98 | } else { 99 | if (char < 120) { 100 | return this._bytes[idx] = char - 56; 101 | } else { 102 | throw MOD_NAME + '.parse() invalid character encountered:' + char + ' at index ' + idx; 103 | } 104 | } 105 | } 106 | } else { 107 | throw MOD_NAME + '.parse() invalid character encountered:' + char + ' at index ' + idx; 108 | } 109 | } 110 | } 111 | }, { 112 | key: 'getInt', 113 | value: function getInt(start, len, unsigned) { 114 | if (DEBUG) console.log(MOD_NAME + '.getInt(' + start + ',' + len + ',' + unsigned.toString() + ')'); 115 | 116 | if (len <= 0) { 117 | return 0; 118 | } 119 | 120 | if (len > 31 || start + len > this._bits) { 121 | throw MOD_NAME + '.getInt() invalid invalid indexes encountered:' + start + ' ' + len; 122 | } 123 | 124 | //let byteCount : number = Math.floor(len / 6); 125 | var bitIdx = start % 6; 126 | var byteIdx = Math.floor(start / 6); 127 | var retVal = 0; 128 | 129 | if (DEBUG) console.log(MOD_NAME + '.getInt() bitIdx:' + bitIdx + ' byteIdx:' + byteIdx); 130 | 131 | var i = void 0; 132 | var bits = 0; 133 | if (bitIdx > 0) { 134 | var rShift = 6 - bitIdx; 135 | retVal = this._getByte(byteIdx++) & 0x3F >> bitIdx; 136 | bits = rShift; 137 | } 138 | 139 | var max = Math.min(len, 25); 140 | while (bits < max) { 141 | retVal = retVal << 6 | this._getByte(byteIdx++); 142 | bits += 6; 143 | } 144 | 145 | if (bits > len) { 146 | retVal >>= bits - len; 147 | } else { 148 | if (bits < len) { 149 | var rest = len - bits; 150 | retVal = retVal << rest | this._getByte(byteIdx) >> 6 - rest; 151 | } 152 | } 153 | 154 | if (!unsigned && len < 32) { 155 | var compl = 1 << len - 1; 156 | if ((retVal & compl) != 0) { 157 | // shit, its negative 158 | retVal = (retVal & ~compl) - compl; 159 | } 160 | } 161 | return retVal; 162 | } 163 | }, { 164 | key: 'getString', 165 | value: function getString(start, len) { 166 | if (len % 6 != 0 || start + len > this._bits || start < 0) { 167 | throw MOD_NAME + '.getString() invalid indexes encountered: start:' + start + ' len:' + len + ' bits:' + this._bits; 168 | } 169 | 170 | if (len === 0) { 171 | return ''; 172 | } 173 | 174 | var bitIdx = start % 6; 175 | var byteIdx = Math.floor(start / 6); 176 | var result = ''; 177 | //SysLog.logWarning(this.getClass().getName() + "::getString(" + start + "," + len + ") bitIdx:" + bitIdx + " byteIdx:" + byteIdx); 178 | 179 | if (bitIdx === 0) { 180 | var endIdx = byteIdx + len / 6; 181 | while (byteIdx < endIdx) { 182 | result += AIS_CHR_TBL[this._getByte(byteIdx++)]; 183 | } 184 | } else { 185 | var hiMask = (0x1 << bitIdx) - 1 << 6 - bitIdx; 186 | var loMask = (0x1 << 6 - bitIdx) - 1; 187 | var _endIdx = byteIdx + len / 6 + 1; 188 | var chrIdx = (this._getByte(byteIdx++) & loMask) << bitIdx; 189 | while (byteIdx < _endIdx) { 190 | var byte = this._getByte(byteIdx++); 191 | result += AIS_CHR_TBL[chrIdx | (byte & hiMask) >> 6 - bitIdx]; 192 | chrIdx = (byte & loMask) << bitIdx; 193 | } 194 | } 195 | return result; 196 | } 197 | }, { 198 | key: 'bits', 199 | get: function get() { 200 | return this._bits; 201 | } 202 | }]); 203 | 204 | return AisBitField; 205 | }(); 206 | 207 | exports.default = AisBitField; 208 | -------------------------------------------------------------------------------- /lib/AisCNBMsg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _AisBitField = require('./AisBitField'); 10 | 11 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 12 | 13 | var _AisMessage2 = require('./AisMessage'); 14 | 15 | var _AisMessage3 = _interopRequireDefault(_AisMessage2); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | /* 26 | * AisParser: A parser for NMEA0183 AIS messages. 27 | * Copyright (C) 2017 Thomas Runte . 28 | * 29 | * This program is free software: you can redistribute it and/or modify 30 | * it under the terms of the Apache License Version 2.0 as published by 31 | * Apache Software foundation. 32 | * 33 | * This program is distributed in the hope that it will be useful, 34 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 | * GNU General Public License for more details. 37 | * 38 | * You should have received a copy of the Apache License Version 2.0 39 | * along with this program. If not, see . 40 | */ 41 | 42 | var MOD_NAME = 'AisCNBMsg'; 43 | 44 | var SUPPORTED_FIELDS = ['aisType', 'channel', 'repeatInd', 'mmsi', 'midCountry', 'midCountryIso', 'mmsiType', 'class', 'navStatus', 'navStatusStr', 'rotStatus', 'rot', 'heading', 'sogStatus', 'sog', 'cog', 'latitude', 'longitude', 'posAccuracy', 'utcTsSec', 'utcTsStatus']; 45 | 46 | var suppValuesValid = false; 47 | var suppValues = {}; 48 | 49 | /* 50 | |============================================================================== 51 | |Field |Len |Description |Member |T|Units 52 | |0-5 | 6 |Message Type |type |u|Constant: 1-3 53 | |6-7 | 2 |Repeat Indicator |repeat |u|Message repeat count 54 | |8-37 |30 |MMSI |mmsi |u|9 decimal digits 55 | |38-41 | 4 |Navigation Status |status |e|See "Navigation Status" 56 | |42-49 | 8 |Rate of Turn (ROT) |turn |I3|See below 57 | |50-59 |10 |Speed Over Ground (SOG) |speed |U1|See below 58 | |60-60 | 1 |Position Accuracy |accuracy |b|See below 59 | |61-88 |28 |Longitude |lon |I4|Minutes/10000 (see below) 60 | |89-115 |27 |Latitude |lat |I4|Minutes/10000 (see below) 61 | |116-127 |12 |Course Over Ground (COG)|course |U1|Relative to true north, 62 | to 0.1 degree precision 63 | |128-136 | 9 |True Heading (HDG) |heading |u|0 to 359 degrees, 64 | 511 = not available. 65 | |137-142 | 6 |Time Stamp |second |u|Second of UTC timestamp 66 | TODO: 67 | |143-144 | 2 |Maneuver Indicator |maneuver |e|See "Maneuver Indicator" 68 | |145-147 | 3 |Spare | |x|Not used 69 | |148-148 | 1 |RAIM flag |raim |b|See below 70 | |149-167 |19 |Radio status |radio |u|See below 71 | |============================================================================== 72 | 73 | */ 74 | 75 | var AisCNBMsg = function (_AisMessage) { 76 | _inherits(AisCNBMsg, _AisMessage); 77 | 78 | function AisCNBMsg(aisType, bitField, channel) { 79 | _classCallCheck(this, AisCNBMsg); 80 | 81 | var _this = _possibleConstructorReturn(this, (AisCNBMsg.__proto__ || Object.getPrototypeOf(AisCNBMsg)).call(this, aisType, bitField, channel)); 82 | 83 | if (bitField.bits >= 144) { 84 | _this._valid = 'VALID'; 85 | } else { 86 | _this._valid = 'INVALID'; 87 | _this._errMsg = 'invalid bitcount for type CNB msg:' + bitField.bits; 88 | } 89 | return _this; 90 | } 91 | 92 | _createClass(AisCNBMsg, [{ 93 | key: '_getRawRot', 94 | value: function _getRawRot() { 95 | return this._bitField.getInt(42, 8, false); 96 | } 97 | }, { 98 | key: '_getRawHeading', 99 | value: function _getRawHeading() { 100 | return this._bitField.getInt(128, 9, true); 101 | } 102 | }, { 103 | key: '_getRawSog', 104 | value: function _getRawSog() { 105 | return this._bitField.getInt(50, 10, true); 106 | } 107 | }, { 108 | key: '_getRawCog', 109 | value: function _getRawCog() { 110 | return this._bitField.getInt(116, 12, true); 111 | } 112 | }, { 113 | key: '_getUtcSec', 114 | value: function _getUtcSec() { 115 | return this._bitField.getInt(137, 6, true); 116 | } 117 | }, { 118 | key: '_getRawLat', 119 | value: function _getRawLat() { 120 | return this._bitField.getInt(89, 27, false); 121 | } 122 | }, { 123 | key: '_getRawLon', 124 | value: function _getRawLon() { 125 | return this._bitField.getInt(61, 28, false); 126 | } 127 | }, { 128 | key: 'class', 129 | get: function get() { 130 | return 'A'; 131 | } 132 | }, { 133 | key: 'supportedValues', 134 | get: function get() { 135 | if (!suppValuesValid) { 136 | SUPPORTED_FIELDS.forEach(function (field) { 137 | var unit = _AisMessage3.default.getUnit(field); 138 | if (unit) { 139 | suppValues[field] = unit; 140 | } else { 141 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 142 | } 143 | }); 144 | suppValuesValid = true; 145 | } 146 | return suppValues; 147 | } 148 | }, { 149 | key: 'navStatus', 150 | get: function get() { 151 | return this._bitField.getInt(38, 4, true); 152 | } 153 | }, { 154 | key: 'posAccuracy', 155 | get: function get() { 156 | return this._bitField.getInt(60, 1, true) === 1; 157 | } 158 | }]); 159 | 160 | return AisCNBMsg; 161 | }(_AisMessage3.default); 162 | 163 | exports.default = AisCNBMsg; 164 | -------------------------------------------------------------------------------- /lib/AisParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | /* 6 | * AisParser: A parser for NMEA0183 AIS messages. 7 | * Copyright (C) 2017 Thomas Runte . 8 | * 9 | * This program is free software: you can redistribute it and/or modify 10 | * it under the terms of the Apache License Version 2.0 as published by 11 | * Apache Software foundation. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the Apache License Version 2.0 19 | * along with this program. If not, see . 20 | */ 21 | 22 | var _AisBitField = require('./AisBitField'); 23 | 24 | var _AisBitField2 = _interopRequireDefault(_AisBitField); 25 | 26 | var _AisMessage = require('./AisMessage'); 27 | 28 | var _AisMessage2 = _interopRequireDefault(_AisMessage); 29 | 30 | var _AisCNBMsg = require('./AisCNBMsg'); 31 | 32 | var _AisCNBMsg2 = _interopRequireDefault(_AisCNBMsg); 33 | 34 | var _Ais04Msg = require('./Ais04Msg'); 35 | 36 | var _Ais04Msg2 = _interopRequireDefault(_Ais04Msg); 37 | 38 | var _Ais05Msg = require('./Ais05Msg'); 39 | 40 | var _Ais05Msg2 = _interopRequireDefault(_Ais05Msg); 41 | 42 | var _Ais18Msg = require('./Ais18Msg'); 43 | 44 | var _Ais18Msg2 = _interopRequireDefault(_Ais18Msg); 45 | 46 | var _Ais19Msg = require('./Ais19Msg'); 47 | 48 | var _Ais19Msg2 = _interopRequireDefault(_Ais19Msg); 49 | 50 | var _Ais21Msg = require('./Ais21Msg'); 51 | 52 | var _Ais21Msg2 = _interopRequireDefault(_Ais21Msg); 53 | 54 | var _Ais24Msg = require('./Ais24Msg'); 55 | 56 | var _Ais24Msg2 = _interopRequireDefault(_Ais24Msg); 57 | 58 | var _Ais27Msg = require('./Ais27Msg'); 59 | 60 | var _Ais27Msg2 = _interopRequireDefault(_Ais27Msg); 61 | 62 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 63 | 64 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 65 | 66 | // TODO: Parser currently rejects multipart messages, if the padbit is != 0 in 67 | // any but the last part. In an AisHub scan 2 messages where encountered 68 | // that where built like that but they were invalid in other ways too, 69 | // so I am hoping to get away like this. 70 | 71 | var MOD_NAME = 'AisParser'; 72 | var DEBUG = false; 73 | 74 | var AisParser = function () { 75 | function AisParser() { 76 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 77 | 78 | _classCallCheck(this, AisParser); 79 | 80 | this._options = options; 81 | this._context = {}; 82 | } 83 | 84 | _createClass(AisParser, [{ 85 | key: 'parse', 86 | value: function parse(sentence) { 87 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 88 | 89 | var checksum = typeof options.checksum !== 'undefined' ? options.checksum : this._options.checksum; 90 | if (checksum && !AisParser.checksumValid(sentence)) { 91 | return _AisMessage2.default.fromError('INVALID', 'Invalid checksum in message: [' + sentence + ']'); 92 | } 93 | return this.parseArray(sentence.split(',')); 94 | } 95 | 96 | // !AIVDM,1,1,,B,14`c;d002grD>PH50hr7RVE000SG,0*74 97 | 98 | }, { 99 | key: 'parseArray', 100 | value: function parseArray(part) { 101 | var parts = part.length; 102 | 103 | if (parts !== 7) { 104 | return _AisMessage2.default.fromError('INVALID', 'Invalid count (!=7) of comma separated elements in message: [' + String(part) + ']'); 105 | } else { 106 | if (part[0] !== '!AIVDM' && part[0] !== '!AIVDO') { 107 | return _AisMessage2.default.fromError('UNSUPPORTED', 'not a supported AIS message:[' + String(part) + ']'); 108 | } 109 | } 110 | 111 | var msgCount = Number(part[1]); 112 | var msgIdx = Number(part[2]); 113 | var msgId = part[3]; 114 | var padBit = Number(part[6].substr(0, 1)); 115 | var aisStr = part[5]; 116 | 117 | if (msgCount > 1) { 118 | if (msgIdx === msgCount) { 119 | var msgParts = this._context[msgId]; 120 | if (!msgParts) { 121 | return _AisMessage2.default.fromError('INVALID', 'missing prior message(s) in partial message:[' + String(part) + ']'); 122 | } 123 | if (msgIdx !== msgParts.idx + 1) { 124 | delete this._context[msgId]; 125 | return _AisMessage2.default.fromError('INVALID', 'sequence violation (skipped or missing message) in partial message:[' + String(part) + ']'); 126 | } 127 | aisStr = msgParts.aisStr + aisStr; 128 | delete this._context[msgId]; 129 | } else { 130 | if (padBit !== 0) { 131 | return _AisMessage2.default.fromError('UNSUPPORTED', 'padbit!=0 not supported in partial message:[' + String(part) + ']'); 132 | } 133 | var _msgParts = this._context[msgId]; 134 | if (msgIdx === 1) { 135 | if (typeof _msgParts !== 'undefined') { 136 | delete this._context[msgId]; 137 | return _AisMessage2.default.fromError('INVALID', 'a message with this sequence and index already exists in partial message:[' + String(part) + ']'); 138 | } 139 | this._context[msgId] = { idx: msgIdx, aisStr: aisStr }; 140 | return _AisMessage2.default.fromError('INCOMPLETE', ''); 141 | } else { 142 | if (!_msgParts) { 143 | return _AisMessage2.default.fromError('INVALID', 'missing prior message in partial message:[' + String(part) + ']'); 144 | } 145 | if (msgIdx !== _msgParts.idx + 1) { 146 | delete this._context[msgId]; 147 | return _AisMessage2.default.fromError('INVALID', 'sequence violation (skipped or missing message) in partial message:[' + String(part) + ']'); 148 | } 149 | _msgParts.idx = msgIdx; 150 | _msgParts.aisStr += aisStr; 151 | return _AisMessage2.default.fromError('INCOMPLETE', ''); 152 | } 153 | } 154 | } else { 155 | if (msgIdx !== 1) { 156 | return _AisMessage2.default.fromError('INVALID', 'invalid message index !=1 in non partial message:[' + String(part) + ']'); 157 | } 158 | } 159 | 160 | try { 161 | var bitField = new _AisBitField2.default(aisStr, padBit); 162 | var aisType = bitField.getInt(0, 6, true); 163 | switch (aisType) { 164 | case 1: 165 | case 2: 166 | case 3: 167 | return new _AisCNBMsg2.default(aisType, bitField, part[4]); 168 | case 4: 169 | return new _Ais04Msg2.default(aisType, bitField, part[4]); 170 | case 5: 171 | return new _Ais05Msg2.default(aisType, bitField, part[4]); 172 | case 18: 173 | return new _Ais18Msg2.default(aisType, bitField, part[4]); 174 | case 19: 175 | return new _Ais19Msg2.default(aisType, bitField, part[4]); 176 | case 21: 177 | return new _Ais21Msg2.default(aisType, bitField, part[4]); 178 | case 24: 179 | return new _Ais24Msg2.default(aisType, bitField, part[4]); 180 | case 27: 181 | return new _Ais27Msg2.default(aisType, bitField, part[4]); 182 | default: 183 | return _AisMessage2.default.fromError('UNSUPPORTED', 'Unsupported ais type ' + aisType + ' in message [' + String(part) + ']', aisType, part[4]); 184 | } 185 | } catch (error) { 186 | return _AisMessage2.default.fromError('INVALID', 'Failed to parse message, error:' + error); 187 | } 188 | } 189 | }], [{ 190 | key: 'checksumValid', 191 | value: function checksumValid(sentence) { 192 | if (!(sentence.startsWith('!AIVDO') || sentence.startsWith('!AIVDM'))) { 193 | return false; 194 | } 195 | 196 | var idx = sentence.indexOf('*'); 197 | if (idx === -1 || idx < 2) { 198 | return false; 199 | } 200 | 201 | var len = idx - 1; 202 | var chkSum = 0; 203 | var i = void 0; 204 | if (DEBUG) console.log(MOD_NAME + '.checksumValid(' + sentence + ') on ' + sentence.substr(1, len)); 205 | for (i = 1; i < idx; i++) { 206 | // if(DEBUG) console.log(MOD_NAME + '.checksumValid() index:' + i + ' value:' + strBuf.readUInt8(i)); 207 | chkSum ^= sentence.charCodeAt(i) & 0xFF; 208 | } 209 | 210 | var chkSumStr = chkSum.toString(16).toUpperCase(); 211 | if (chkSumStr.length < 2) { 212 | chkSumStr = '0' + chkSumStr; 213 | } 214 | if (DEBUG && chkSumStr !== sentence.substr(idx + 1)) { 215 | console.warn(MOD_NAME + '.checksumValid(' + sentence + ') ' + chkSumStr + '!==' + sentence.substr(idx + 1)); 216 | } 217 | return chkSumStr === sentence.substr(idx + 1); 218 | } 219 | }]); 220 | 221 | return AisParser; 222 | }(); 223 | 224 | module.exports = AisParser; 225 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | PATH := node_modules/.bin:$(PATH) 2 | # TODO: use the shell that is available bash / ash or install bash in dockerfile 3 | 4 | SRC_FILES := $(shell find src/ -type f | grep -v __tests__) 5 | LIB_FILES := $(patsubst src/%.js, lib/%.js, $(SRC_FILES)) 6 | BABEL_OPTS := --plugins transform-flow-strip-types --presets env 7 | 8 | .PHONY: all 9 | 10 | all: dirs $(LIB_FILES) 11 | 12 | dirs: 13 | @mkdir -p lib 14 | 15 | clean: 16 | rm -r lib 17 | 18 | lib/%.js: src/%.js makefile 19 | @mkdir -p $(dir $@) 20 | babel $(BABEL_OPTS) -o $@ $< 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aisparser", 3 | "version": "0.0.13", 4 | "private": false, 5 | "description": "parser for NMEA0183 AIS messages", 6 | "keywords": [ 7 | "parser AIS NMEA NMEA0183" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/samothx/AisParser.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/samothx/AisParser/issues", 15 | "email": "support@etnur.net" 16 | }, 17 | "license": "Apache-2.0", 18 | "main": "index.js", 19 | "engines": { 20 | "node": ">=6" 21 | }, 22 | "scripts": { 23 | "build": "make", 24 | "transpile": "make", 25 | "prepublishOnly": "make", 26 | "flow": "flow; test $? -eq 0 -o $? -eq 2", 27 | "test": "jest" 28 | }, 29 | "devDependencies": { 30 | "babel-cli": "^6.26.0", 31 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 32 | "babel-preset-env": "^1.6.1", 33 | "flow-bin": "^0.61.0", 34 | "jest": "^22.4.3", 35 | "random-seed": "^0.3.0" 36 | }, 37 | "homepage": "https://github.com/samothx/AisParser#readme", 38 | "directories": { 39 | "doc": "docs", 40 | "test": "test" 41 | }, 42 | "dependencies": {}, 43 | "author": "Thomas Runte (http://www.etnur.net)", 44 | "jest" : { 45 | "testPathIgnorePatterns" : ["/node_modules/","/testHelper/"] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /samples/Sample.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * AisParser: A parser for NMEA0183 AIS messages. 4 | * Copyright (C) 2017 Thomas Runte . 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the Apache License Version 2.0 as published by 8 | * Apache Software foundation. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the Apache License Version 2.0 16 | * along with this program. If not, see . 17 | */ 18 | 19 | var AisParser = require('../index'); 20 | 21 | var parser = new AisParser({ checksum : true }); 22 | 23 | var sentences = [ 24 | '!AIVDM,1,1,,B,14`c;d002grD>PH50hr7RVE000SG,0*74', 25 | '!AIVDM,1,1,,B,34hwN60Oh3rCwib56`qJtbL<0000,0*12', 26 | '!AIVDM,1,1,,B,15TPq@0Oj0rClEv53P9HWVn<283C,0*51', 27 | '!AIVDO,1,1,,,B39i>1000nTu;gQAlBj:wwS5kP06,0*5D', 28 | '!AIVDM,1,1,,A,35SrP>5P00rCQAL5:KA8b0000rCgTH58DU6KpJj0`0>,0*37', 33 | '!AIVDO,1,1,,,B39i>1001FTu;bQAlAMscwe5kP06,0*3E', 34 | '!AIVDM,1,1,,B,15Bs:H8001JCUE852dB. 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import type {SuppValues} from './AisMessage'; 23 | 24 | const MOD_NAME = 'Ais04Msg'; 25 | const SUPPORTED_FIELDS = [ 26 | 'aisType', 27 | 'channel', 28 | 'repeatInd', 29 | 'mmsi', 30 | 'midCountry', 31 | 'midCountryIso', 32 | 'mmsiType', 33 | 'latitude', 34 | 'longitude', 35 | 'posAccuracy', 36 | 'utcYear', 37 | 'utcMonth', 38 | 'utcDay', 39 | 'utcHour', 40 | 'utcMinute', 41 | 'utcSecond', 42 | 'epfd' 43 | ]; 44 | 45 | let suppValuesValid = false; 46 | let suppValues : SuppValues = {}; 47 | 48 | /* 49 | |============================================================================== 50 | |Field |Len |Description |Member |T|Units 51 | |0-5 | 6 |Message Type |type |u|Constant: 4 52 | |6-7 | 2 |Repeat Indicator |repeat |u|As in Common Navigation Block 53 | |8-37 | 30 |MMSI |mmsi |u|9 decimal digits 54 | |38-51 | 14 |Year (UTC) |year |u|UTC, 1-999, 0 = N/A (default) 55 | |52-55 | 4 |Month (UTC) |month |u|1-12; 0 = N/A (default) 56 | |56-60 | 5 |Day (UTC) |day |u|1-31; 0 = N/A (default) 57 | |61-65 | 5 |Hour (UTC) |hour |u|0-23; 24 = N/A (default) 58 | |66-71 | 6 |Minute (UTC) |minute |u|0-59; 60 = N/A (default) 59 | |72-77 | 6 |Second (UTC) |second |u|0-59; 60 = N/A (default) 60 | |78-78 | 1 |Fix quality |accuracy |b|As in Common Navigation Block 61 | |79-106 | 28 |Longitude |lon |I4|As in Common Navigation Block 62 | |107-133 | 27 |Latitude |lat |I4|As in Common Navigation Block 63 | |134-137 | 4 |Type of EPFD |epfd |e|See "EPFD Fix Types" 64 | |138-147 | 10 |Spare | |x|Not used 65 | // TODO 66 | |148-148 | 1 |RAIM flag |raim |b|As for common navigation block 67 | |149-167 | 19 |SOTDMA state |radio |u|As in same bits for Type 1 68 | |============================================================================== 69 | */ 70 | 71 | export default class Ais04Msg extends AisMessage { 72 | constructor(aisType : number,bitField : AisBitField, channel : string) { 73 | super(aisType,bitField,channel); 74 | // TODO: check bitcount 75 | if(bitField.bits >= 167) { 76 | this._valid = 'VALID'; 77 | } else { 78 | this._valid = 'INVALID'; 79 | this._errMsg = 'invalid bitcount for type 04 msg:' + bitField.bits; 80 | } 81 | } 82 | 83 | get supportedValues() : SuppValues { 84 | if(!suppValuesValid) { 85 | SUPPORTED_FIELDS.forEach((field)=>{ 86 | let unit = AisMessage.getUnit(field); 87 | if(unit) { 88 | suppValues[field] = unit; 89 | } else { 90 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 91 | }}); 92 | suppValuesValid = true; 93 | } 94 | return suppValues; 95 | } 96 | 97 | get utcYear() : number { 98 | return this._bitField.getInt(38,14,true); 99 | } 100 | 101 | get utcMonth() : number { 102 | return this._bitField.getInt(52,4,true); 103 | } 104 | 105 | get utcDay() : number { 106 | return this._bitField.getInt(56,5,true); 107 | } 108 | 109 | get utcHour() : number { 110 | return this._bitField.getInt(61,5,true); 111 | } 112 | 113 | get utcMinute() : number { 114 | return this._bitField.getInt(66,6,true); 115 | } 116 | 117 | get utcSecond() : number { 118 | return this._bitField.getInt(72,6,true); 119 | } 120 | 121 | get posAccuracy() : boolean { 122 | return this._bitField.getInt(78, 1, true) === 1; 123 | } 124 | 125 | _getRawLat() : number { 126 | return this._bitField.getInt(107,27,false); 127 | } 128 | 129 | _getRawLon() : number { 130 | return this._bitField.getInt(79,28,false); 131 | } 132 | 133 | get epfd() : number { 134 | return this._bitField.getInt(134,4,true); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/Ais05Msg.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import type {SuppValues} from './AisMessage'; 23 | 24 | const MOD_NAME = 'Ais05Msg'; 25 | const SUPPORTED_FIELDS = [ 26 | 'aisType', 27 | 'channel', 28 | 'repeatInd', 29 | 'mmsi', 30 | 'midCountry', 31 | 'midCountryIso', 32 | 'mmsiType', 33 | 'callSign', 34 | 'name', 35 | 'aisVer', 36 | 'imo', 37 | 'shipType', 38 | 'shipTypeStr', 39 | 'dimToBow', 40 | 'dimToBowStatus', 41 | 'dimToStern', 42 | 'dimToSternStatus', 43 | 'dimToPort', 44 | 'dimToPortStatus', 45 | 'dimToStbrd', 46 | 'dimToStbrdStatus', 47 | 'epfd', 48 | 'epfdStr', 49 | 'etaMonth', 50 | 'etaDay', 51 | 'etaHour', 52 | 'etaMinute', 53 | 'draught', 54 | 'destination', 55 | ]; 56 | 57 | let suppValuesValid = false; 58 | let suppValues : SuppValues = {}; 59 | 60 | 61 | /* 62 | |============================================================================== 63 | |Field |Len |Description |Member/Type |T|Encoding 64 | |0-5 | 6 |Message Type |type |u|Constant: 5 65 | |6-7 | 2 |Repeat Indicator |repeat |u|Message repeat count 66 | |8-37 | 30 |MMSI |mmsi |u|9 digits 67 | |38-39 | 2 |AIS Version |ais_version |u|0=<>, 68 | 1-3 = future editions 69 | |40-69 | 30 |IMO Number |imo |u|IMO ship ID number 70 | |70-111 | 42 |Call Sign |callsign |t|7 six-bit characters 71 | |112-231 |120 |Vessel Name |shipname |t|20 six-bit characters 72 | |232-239 | 8 |Ship Type |shiptype |e|See "Codes for Ship Type" 73 | |240-248 | 9 |Dimension to Bow |to_bow |u|Meters 74 | |249-257 | 9 |Dimension to Stern |to_stern |u|Meters 75 | |258-263 | 6 |Dimension to Port |to_port |u|Meters 76 | |264-269 | 6 |Dimension to Starboard |to_starboard |u|Meters 77 | |270-273 | 4 |Position Fix Type |epfd |e|See "EPFD Fix Types" 78 | |274-277 | 4 |ETA month (UTC) |month |u|1-12, 0=N/A (default) 79 | |278-282 | 5 |ETA day (UTC) |day |u|1-31, 0=N/A (default) 80 | |283-287 | 5 |ETA hour (UTC) |hour |u|0-23, 24=N/A (default) 81 | |288-293 | 6 |ETA minute (UTC) |minute |u|0-59, 60=N/A (default) 82 | |294-301 | 8 |Draught |draught |U1|Meters/10 83 | |302-421 |120 |Destination |destination |t|20 6-bit characters 84 | TODO: 85 | |422-422 | 1 |DTE |dte |b|0=Data terminal ready, 86 | 1=Not ready (default). 87 | |423-423 | 1 |Spare | |x|Not used 88 | |============================================================================== 89 | */ 90 | 91 | export default class Ais05Msg extends AisMessage { 92 | constructor(aisType : number,bitField : AisBitField, channel : string) { 93 | super(aisType,bitField,channel); 94 | if(bitField.bits >= 423) { 95 | this._valid = 'VALID'; 96 | } else { 97 | this._valid = 'INVALID'; 98 | this._errMsg = 'invalid bitcount for type 05 msg:' + bitField.bits; 99 | } 100 | } 101 | 102 | get supportedValues() : SuppValues { 103 | if(!suppValuesValid) { 104 | SUPPORTED_FIELDS.forEach((field)=>{ 105 | let unit = AisMessage.getUnit(field); 106 | if(unit) { 107 | suppValues[field] = unit; 108 | } else { 109 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 110 | }}); 111 | suppValuesValid = true; 112 | } 113 | return suppValues; 114 | } 115 | 116 | get callSign() : string { 117 | return this._formatStr(this._bitField.getString(70,42)); 118 | } 119 | 120 | get name() : string { 121 | return this._formatStr(this._bitField.getString(112,120)); 122 | } 123 | 124 | get aisVer() : number { 125 | return this._bitField.getInt(38,2,true); 126 | } 127 | 128 | get imo() : number { 129 | return this._bitField.getInt(40,30,true); 130 | } 131 | 132 | get shipType() : number { 133 | return this._bitField.getInt(232,8,true); 134 | } 135 | 136 | _getDimToBow() : number { 137 | return this._bitField.getInt(240,9,true); 138 | } 139 | 140 | _getDimToStern() : number { 141 | return this._bitField.getInt(249,9,true); 142 | } 143 | 144 | _getDimToPort() : number { 145 | return this._bitField.getInt(258,6,true); 146 | } 147 | 148 | _getDimToStbrd() : number { 149 | return this._bitField.getInt(264,6,true); 150 | } 151 | 152 | get epfd() : number { 153 | return this._bitField.getInt(270,4,true); 154 | } 155 | 156 | get etaMonth() : number { 157 | return this._bitField.getInt(274,4,true) || NaN; 158 | } 159 | 160 | get etaDay() : number { 161 | return this._bitField.getInt(278,5,true) || NaN; 162 | } 163 | 164 | get etaHour() : number { 165 | return this._bitField.getInt(283,5,true) || NaN; 166 | } 167 | 168 | get etaMinute() : number { 169 | return this._bitField.getInt(288,6,true) || NaN; 170 | } 171 | 172 | get draught() : number { 173 | return this._bitField.getInt(294,8,true) / 10; 174 | } 175 | 176 | get destination() : string { 177 | return this._formatStr(this._bitField.getString(302,120)); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Ais18Msg.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import type {SuppValues} from './AisMessage'; 23 | 24 | const MOD_NAME = 'Ais18Msg'; 25 | 26 | 27 | /* 28 | |============================================================================== 29 | |Field |Len |Description |Member |T|Units 30 | |0-5 | 6 |Message Type |type |u|Constant: 18 31 | |6-7 | 2 |Repeat Indicator |repeat |u|As in Common Navigation Block 32 | |8-37 |30 |MMSI |mmsi |u|9 decimal digits 33 | |38-45 | 8 |Regional Reserved |reserved |x|Not used 34 | |46-55 |10 |Speed Over Ground |speed |u|As in common navigation block 35 | |56-56 | 1 |Position Accuracy |accuracy |b|See below 36 | |57-84 |28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 37 | |85-111 |27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 38 | |112-123 |12 |Course Over Ground |course |U1|0.1 degrees from true north 39 | |124-132 | 9 |True Heading |heading |u|0 to 359 degrees, 511 = N/A 40 | |133-138 | 6 |Time Stamp |second |u|Second of UTC timestamp. 41 | TODO: 42 | |139-140 | 2 |Regional reserved |regional |u|Uninterpreted 43 | |141-141 | 1 |CS Unit |cs |b|0=Class B SOTDMA unit 44 | 1=Class B CS (Carrier Sense) unit 45 | |142-142 | 1 |Display flag |display |b|0=No visual display, 46 | 1=Has display, 47 | (Probably not reliable). 48 | |143-143 | 1 |DSC Flag |dsc |b|If 1, unit is attached to a VHF 49 | voice radio with DSC capability. 50 | |144-144 | 1 |Band flag |band |b|Base stations can command units 51 | to switch frequency. If this flag 52 | is 1, the unit can use any part 53 | of the marine channel. 54 | |145-145 | 1 |Message 22 flag |msg22 |b|If 1, unit can accept a channel 55 | assignment via Message Type 22. 56 | |146-146 | 1 |Assigned |assigned |b|Assigned-mode flag: 57 | 0 = autonomous mode (default), 58 | 1 = assigned mode. 59 | |147-147 | 1 |RAIM flag |raim |b|As for common navigation block 60 | |148-167 |20 |Radio status |radio |u|See <> for details. 61 | |============================================================================== 62 | */ 63 | 64 | const SUPPORTED_FIELDS = [ 65 | 'aisType', 66 | 'channel', 67 | 'repeatInd', 68 | 'mmsi', 69 | 'midCountry', 70 | 'midCountryIso', 71 | 'mmsiType', 72 | 'heading', 73 | 'sogStatus', 74 | 'sog', 75 | 'cog', 76 | 'latitude', 77 | 'longitude', 78 | 'posAccuracy', 79 | 'utcTsSec', 80 | 'utcTsStatus', 81 | ]; 82 | let suppValuesValid = false; 83 | let suppValues : SuppValues = {}; 84 | 85 | 86 | export default class Ais18Msg extends AisMessage { 87 | constructor(aisType : number,bitField : AisBitField, channel : string) { 88 | super(aisType,bitField,channel); 89 | if(bitField.bits >= 167) { 90 | this._valid = 'VALID'; 91 | } else { 92 | this._valid = 'INVALID'; 93 | this._errMsg = 'invalid bitcount for type 18 msg:' + bitField.bits; 94 | } 95 | } 96 | 97 | 98 | get supportedValues() : SuppValues { 99 | if(!suppValuesValid) { 100 | SUPPORTED_FIELDS.forEach((field)=>{ 101 | let unit = AisMessage.getUnit(field); 102 | if(unit) { 103 | suppValues[field] = unit; 104 | } else { 105 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 106 | }}); 107 | suppValuesValid = true; 108 | } 109 | return suppValues; 110 | } 111 | 112 | get class() : string { 113 | return 'B'; 114 | } 115 | 116 | _getRawHeading() : number { 117 | return this._bitField.getInt(124, 9, true); 118 | } 119 | 120 | _getRawSog() : number { 121 | return this._bitField.getInt(46, 10, true); 122 | } 123 | 124 | _getRawCog() : number { 125 | return this._bitField.getInt(112, 12, true); 126 | } 127 | 128 | get posAccuracy() : boolean { 129 | return this._bitField.getInt(56, 1, true) === 1; 130 | } 131 | 132 | _getUtcSec() : number { 133 | return this._bitField.getInt(133,6,true); 134 | } 135 | 136 | _getRawLat() : number { 137 | return this._bitField.getInt(85,27,false); 138 | } 139 | 140 | _getRawLon() : number { 141 | return this._bitField.getInt(57,28,false); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Ais19Msg.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import type {SuppValues} from './AisMessage'; 23 | 24 | const MOD_NAME = 'Ais21Msg'; 25 | /* 26 | |============================================================================== 27 | |Field |Len |Description |Member |T|Units 28 | |0-5 | 6 |Message Type |type |u|Constant: 19 29 | |6-7 | 2 |Repeat Indicator |repeat |u|As in CNN 30 | |8-37 | 30 |MMSI |mmsi |u|9 digits 31 | |38-45 | 8 |Regional Reserved |reserved |u| 32 | |46-55 | 10 |Speed Over Ground |speed |u|As in CNB. 33 | |56-56 | 1 |Position Accuracy |accuracy |b|As in CNB. 34 | |57-84 | 28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 35 | |85-111 | 27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 36 | |112-123 | 12 |Course Over Ground |course |U1|Relative to true north, 37 | units of 0.1 degrees 38 | |124-132 | 9 |True Heading |heading |u|0 to 359 degrees, 39 | 511 = N/A 40 | |133-138 | 6 |Time Stamp |second |u|Second of UTC timestamp. 41 | |139-142 | 4 |Regional reserved |regional |u|Uninterpreted 42 | |143-262 |120 |Name |shipname |s|20 6-bit characters 43 | |263-270 | 8 |Type of ship and cargo |shiptype |u|As in Message 5 44 | |271-279 | 9 |Dimension to Bow |to_bow |u|Meters 45 | |280-288 | 9 |Dimension to Stern |to_stern |u|Meters 46 | |289-294 | 6 |Dimension to Port |to_port |u|Meters 47 | |295-300 | 6 |Dimension to Starboard |to_starboard |u|Meters 48 | |301-304 | 4 |Position Fix Type |epfd |e|See "EPFD Fix Types" 49 | |305-305 | 1 |RAIM flag |raim |b|As in CNB. 50 | |306-306 | 1 |DTE |dte |b|0=Data terminal ready, 51 | 1=Not ready (default). 52 | |307-307 | 1 |Assigned mode flag |assigned |u|See <> for details 53 | |308-311 | 4 |Spare | |x|Unused, should be zero 54 | |============================================================================== 55 | */ 56 | const SUPPORTED_FIELDS = [ 57 | 'aisType', 58 | 'channel', 59 | 'repeatInd', 60 | 'mmsi', 61 | 'midCountry', 62 | 'midCountryIso', 63 | 'mmsiType', 64 | 'class', 65 | 'heading', 66 | 'sogStatus', 67 | 'sog', 68 | 'cog', 69 | 'latitude', 70 | 'longitude', 71 | 'posAccuracy', 72 | 'utcTsSec', 73 | 'utcTsStatus', 74 | 'name', 75 | 'shipType', 76 | 'shipTypeStr', 77 | 'dimToBow', 78 | 'dimToBowStatus', 79 | 'dimToStern', 80 | 'dimToSternStatus', 81 | 'dimToPort', 82 | 'dimToPortStatus', 83 | 'dimToStbrd', 84 | 'dimToStbrdStatus', 85 | 'epfd', 86 | 'epfdStr' 87 | ]; 88 | 89 | let suppValuesValid = false; 90 | let suppValues : SuppValues = {}; 91 | 92 | 93 | export default class Ais19Msg extends AisMessage { 94 | constructor(aisType : number,bitField : AisBitField, channel : string) { 95 | super(aisType,bitField,channel); 96 | if(bitField.bits >= 311) { 97 | this._valid = 'VALID'; 98 | } else { 99 | this._valid = 'INVALID'; 100 | this._errMsg = 'invalid bitcount for type 19 msg:' + bitField.bits; 101 | } 102 | } 103 | 104 | 105 | get supportedValues() : SuppValues { 106 | if(!suppValuesValid) { 107 | SUPPORTED_FIELDS.forEach((field)=>{ 108 | let unit = AisMessage.getUnit(field); 109 | if(unit) { 110 | suppValues[field] = unit; 111 | } else { 112 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 113 | }}); 114 | suppValuesValid = true; 115 | } 116 | return suppValues; 117 | } 118 | 119 | get class() : string { 120 | return 'B'; 121 | } 122 | 123 | // |124-132 | 9 |True Heading |heading |u|0 to 359 degrees, 124 | _getRawHeading() : number { 125 | return this._bitField.getInt(124, 9, true); 126 | } 127 | 128 | //|46-55 | 10 |Speed Over Ground |speed |u|As in CNB. 129 | _getRawSog() : number { 130 | return this._bitField.getInt(46, 10, true); 131 | } 132 | 133 | // |112-123 | 12 |Course Over Ground |course |U1|Relative to true north, 134 | _getRawCog() : number { 135 | return this._bitField.getInt(112, 12, true); 136 | } 137 | 138 | // |56-56 | 1 |Position Accuracy |accuracy |b|As in CNB. 139 | get posAccuracy() : boolean { 140 | return this._bitField.getInt(56, 1, true) === 1; 141 | } 142 | 143 | // |133-138 | 6 |Time Stamp |second |u|Second of UTC timestamp. 144 | _getUtcSec() : number { 145 | return this._bitField.getInt(133,6,true); 146 | } 147 | 148 | // |85-111 | 27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 149 | _getRawLat() : number { 150 | return this._bitField.getInt(85,27,false); 151 | } 152 | 153 | // |57-84 | 28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 154 | _getRawLon() : number { 155 | return this._bitField.getInt(57,28,false); 156 | } 157 | 158 | // |143-262 |120 |Name |shipname |s|20 6-bit characters 159 | get name() : string { 160 | return this._formatStr(this._bitField.getString(143,120)); 161 | } 162 | 163 | // |263-270 | 8 |Type of ship and cargo |shiptype |u|As in Message 5 164 | get shipType() : number { 165 | return this._bitField.getInt(263,8,true); 166 | } 167 | 168 | // |271-279 | 9 |Dimension to Bow |to_bow |u|Meters 169 | _getDimToBow() : number { 170 | return this._bitField.getInt(271,9,true); 171 | } 172 | 173 | // |280-288 | 9 |Dimension to Stern |to_stern |u|Meters 174 | _getDimToStern() : number { 175 | return this._bitField.getInt(280,9,true); 176 | } 177 | 178 | // |289-294 | 6 |Dimension to Port |to_port |u|Meters 179 | _getDimToPort() : number { 180 | return this._bitField.getInt(289,6,true); 181 | } 182 | 183 | // |295-300 | 6 |Dimension to Starboard |to_starboard |u|Meters 184 | _getDimToStbrd() : number { 185 | return this._bitField.getInt(295,6,true); 186 | } 187 | 188 | // |301-304 | 4 |Position Fix Type |epfd |e|See "EPFD Fix Types" 189 | get epfd() : number { 190 | return this._bitField.getInt(301,4,true); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Ais21Msg.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import type {SuppValues} from './AisMessage'; 23 | 24 | const MOD_NAME = 'Ais21Msg'; 25 | /* 26 | |============================================================================== 27 | |Field |Len |Description |Member |T|Units 28 | |0-5 | 6 |Message Type |type |u|Constant: 21 29 | |6-7 | 2 |Repeat Indicator |repeat |u|As in CNB 30 | |8-37 |30 |MMSI |mmsi |u|9 digits 31 | |38-42 | 5 |Aid type |aid_type |e|See "Navaid Types" 32 | |43-162 1|120 |Name |name |t|Name in sixbit chars 33 | |163-163 | 1 |Position Accuracy |accuracy |b|As in CNB 34 | |164-191 |28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 35 | |192-218 |27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 36 | |219-227 | 9 |Dimension to Bow |to_bow |u|Meters 37 | |228-236 | 9 |Dimension to Stern |to_stern |u|Meters 38 | |237-242 | 6 |Dimension to Port |to_port |u|Meters 39 | |243-248 | 6 |Dimension to Starboard |to_starboard|u|Meters 40 | |249-252 | 4 |Type of EPFD |epfd |e|As in Message Type 4 41 | |253-258 | 6 |UTC Second |second |u|As in Message Type 5 42 | |259-259 | 1 |Off-Position Indicator |off_position|b|See Below 43 | |260-267 | 8 |Regional reserved |regional |u|Uninterpreted 44 | |268-268 | 1 |RAIM flag |raim |b|As in CNB 45 | |269-269 | 1 |Virtual-aid flag |virtual_aid |b|See Below 46 | |270-270 | 1 |Assigned-mode flag |assigned |b|See <> for details 47 | |271-271 | 1 |Spare | |x|Not used 48 | |272-360 |88 |Name Extension | |t|See Below 49 | |============================================================================== 50 | */ 51 | 52 | const SUPPORTED_FIELDS = [ 53 | 'aisType', 54 | 'channel', 55 | 'repeatInd', 56 | 'mmsi', 57 | 'midCountry', 58 | 'midCountryIso', 59 | 'mmsiType', 60 | 'name', 61 | 'latitude', 62 | 'longitude', 63 | 'posAccuracy', 64 | 'dimToBow', 65 | 'dimToBowStatus', 66 | 'dimToStern', 67 | 'dimToSternStatus', 68 | 'dimToPort', 69 | 'dimToPortStatus', 70 | 'dimToStbrd', 71 | 'dimToStbrdStatus', 72 | 'length', 73 | 'width', 74 | 'epfd', 75 | 'epfdStr', 76 | 'utcTsSec', 77 | 'utcTsStatus', 78 | 'offPosInd', 79 | 'aidType', 80 | 'aidTypeStr', 81 | 'nameExt' 82 | ]; 83 | 84 | let suppValuesValid = false; 85 | let suppValues : SuppValues = {}; 86 | 87 | export default class Ais21Msg extends AisMessage { 88 | constructor(aisType : number,bitField : AisBitField, channel : string) { 89 | super(aisType,bitField,channel); 90 | if(bitField.bits >= 271) { 91 | this._valid = 'VALID'; 92 | } else { 93 | this._valid = 'INVALID'; 94 | this._errMsg = 'invalid bitcount for type 21 msg:' + bitField.bits; 95 | } 96 | } 97 | 98 | 99 | get supportedValues() : SuppValues { 100 | if(!suppValuesValid) { 101 | SUPPORTED_FIELDS.forEach((field)=>{ 102 | let unit = AisMessage.getUnit(field); 103 | if(unit) { 104 | suppValues[field] = unit; 105 | } else { 106 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 107 | }}); 108 | suppValuesValid = true; 109 | } 110 | return suppValues; 111 | } 112 | 113 | // |38-42 | 5 |Aid type |aid_type |e|See "Navaid Types" 114 | get aidType() : number { 115 | return this._bitField.getInt(38,5,true); 116 | } 117 | 118 | // |43-162 1|120 |Name |name |t|Name in sixbit chars 119 | get name() : string { 120 | return this._formatStr(this._bitField.getString(43,162)); 121 | } 122 | 123 | // |163-163 | 1 |Position Accuracy |accuracy |b|As in CNB 124 | get posAccuracy() : boolean { 125 | return this._bitField.getInt(163, 1, true) === 1; 126 | } 127 | 128 | // |164-191 |28 |Longitude |lon |I4|Minutes/10000 (as in CNB) 129 | _getRawLon() : number { 130 | return this._bitField.getInt(164,28,false); 131 | } 132 | 133 | //|192-218 |27 |Latitude |lat |I4|Minutes/10000 (as in CNB) 134 | _getRawLat() : number { 135 | return this._bitField.getInt(192,27,false); 136 | } 137 | 138 | // |219-227 | 9 |Dimension to Bow |to_bow |u|Meters 139 | _getDimToBow() : number { 140 | return this._bitField.getInt(219,9,true); 141 | } 142 | 143 | // |228-236 | 9 |Dimension to Stern |to_stern |u|Meters 144 | _getDimToStern() : number { 145 | return this._bitField.getInt(228,9,true); 146 | } 147 | 148 | // |237-242 | 6 |Dimension to Port |to_port |u|Meters 149 | _getDimToPort() : number { 150 | return this._bitField.getInt(237,6,true); 151 | } 152 | 153 | // |243-248 | 6 |Dimension to Starboard |to_starboard|u|Meters 154 | _getDimToStbrd() : number { 155 | return this._bitField.getInt(243,6,true); 156 | } 157 | 158 | // |249-252 | 4 |Type of EPFD |epfd |e|As in Message Type 4 159 | get epfd() : number { 160 | return this._bitField.getInt(249,4,true); 161 | } 162 | 163 | // |253-258 | 6 |UTC Second |second |u|As in Message Type 5 164 | _getUtcSec() : number { 165 | return this._bitField.getInt(253,6,true); 166 | } 167 | 168 | // |259-259 | 1 |Off-Position Indicator |off_position|b|See Below 169 | get offPosInd() : 'IN_POS' | 'OFF_POS' | 'NA' { 170 | if(this._getUtcSec() < 60) { 171 | return (this._bitField.getInt(163, 1, true) === 0) ? 'IN_POS' : 'OFF_POS'; 172 | } else { 173 | return 'NA'; 174 | } 175 | } 176 | 177 | // |272-360 |88 |Name Extension | |t|See Below 178 | get nameExt() : string { 179 | if(this._bitField.bits > 272) { 180 | let chars : number = Math.floor((this._bitField.bits - 272) / 6); 181 | if(chars > 0) { 182 | return this._formatStr(this._bitField.getString(272,chars * 6)); 183 | } 184 | } 185 | return ''; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Ais24Msg.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import type {SuppValues} from './AisMessage'; 23 | 24 | const MOD_NAME = 'Ais24Msg'; 25 | 26 | const SUPPORTED_FIELDS_A = [ 27 | 'aisType', 28 | 'channel', 29 | 'repeatInd', 30 | 'mmsi', 31 | 'midCountry', 32 | 'midCountryIso', 33 | 'mmsiType', 34 | 'partNo', 35 | 'name' ]; 36 | 37 | const SUPPORTED_FIELDS_B_NO_TENDER = [ 38 | 'aisType', 39 | 'channel', 40 | 'repeatInd', 41 | 'mmsi', 42 | 'midCountry', 43 | 'midCountryIso', 44 | 'mmsiType', 45 | 'partNo', 46 | 'shipType', 47 | 'callSign', 48 | 'vendorId', 49 | 'dimToBow', 50 | 'dimToBowStatus', 51 | 'dimToStern', 52 | 'dimToSternStatus', 53 | 'dimToPort', 54 | 'dimToPortStatus', 55 | 'dimToStbrd', 56 | 'dimToStbrdStatus' 57 | ]; 58 | 59 | const SUPPORTED_FIELDS_B_TENDER = [ 60 | 'aisType', 61 | 'channel', 62 | 'repeatInd', 63 | 'mmsi', 64 | 'midCountry', 65 | 'midCountryIso', 66 | 'mmsiType', 67 | 'partNo', 68 | 'shipType', 69 | 'callSign', 70 | 'vendorId', 71 | 'mothershipMmsi']; 72 | 73 | let suppValuesValidA = false; 74 | let suppValuesA : SuppValues = {}; 75 | let suppValuesValidBNT = false; 76 | let suppValuesBT : SuppValues = {}; 77 | let suppValuesValidBT = false; 78 | let suppValuesBNT : SuppValues = {}; 79 | 80 | /* 81 | |============================================================================== 82 | |Field |Len |Description | Member |T|Units 83 | |0-5 | 6 | Message Type | type |u|Constant: 24 84 | |6-7 | 2 | Repeat Indicator | repeat |u|As in CNB 85 | |8-37 | 30 | MMSI | mmsi |u|9 digits 86 | |38-39 | 2 | Part Number | partno |u|0-1 87 | |40-159 |120 | Vessel Name | shipname |t|(Part A) 20 sixbit chars 88 | |160-167 | 8 | Spare | |x|(Part A) Not used 89 | |40-47 | 8 | Ship Type | shiptype |e|(Part B) See "Ship Types" 90 | |48-89 | 42 | Vendor ID | vendorid |t|(Part B) 7 six-bit chars 91 | |90-131 | 42 | Call Sign | callsign |t|(Part B) As in Message Type 5 92 | |132-140 | 9 | Dimension to Bow | to_bow |u|(Part B) Meters 93 | |141-149 | 9 | Dimension to Stern | to_stern |u|(Part B) Meters 94 | |150-155 | 6 | Dimension to Port | to_port |u|(Part B) Meters 95 | |156-161 | 6 | Dimension to Starboard| to_starboard |u|(Part B) Meters 96 | |132-161 | 30 | Mothership MMSI | mothership_mmsi|u|(Part B) See below 97 | |162-167 | 6 | Spare | |x|(Part B) Not used 98 | |=============================================================================== 99 | */ 100 | 101 | export type PartNo = 0 | 1; 102 | 103 | export default class Ais24Msg extends AisMessage { 104 | _partNo : ?PartNo; 105 | _tender : ?boolean; 106 | 107 | constructor(aisType : number,bitField : AisBitField, channel : string) { 108 | super(aisType,bitField,channel); 109 | if(bitField.bits >= 39) { 110 | this._partNo = this._bitField.getInt(38,2,true) ? 1 : 0; 111 | 112 | if(((this._partNo === 0) && (bitField.bits >= 159)) || (bitField.bits >= 161)) { 113 | this._valid = 'VALID'; 114 | return; 115 | } 116 | } 117 | this._valid = 'INVALID'; 118 | this._errMsg = 'invalid bitcount for type 24 msg:' + bitField.bits; 119 | } 120 | 121 | get supportedValues() : SuppValues { 122 | if(this.partNo === 0) { 123 | if(!suppValuesValidA) { 124 | SUPPORTED_FIELDS_A.forEach((field)=>{ 125 | let unit = AisMessage.getUnit(field); 126 | if(unit) { 127 | suppValuesA[field] = unit; 128 | } else { 129 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 130 | }}); 131 | suppValuesValidA = true; 132 | } 133 | return suppValuesA; 134 | } else { 135 | if(this._isTender()) { 136 | if(!suppValuesValidBT) { 137 | SUPPORTED_FIELDS_B_TENDER.forEach((field)=>{ 138 | let unit = AisMessage.getUnit(field); 139 | if(unit) { 140 | suppValuesBT[field] = unit; 141 | } else { 142 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 143 | }}); 144 | suppValuesValidBT = true; 145 | } 146 | return suppValuesBT; 147 | } else { 148 | if(!suppValuesValidBNT) { 149 | SUPPORTED_FIELDS_B_NO_TENDER.forEach((field)=>{ 150 | let unit = AisMessage.getUnit(field); 151 | if(unit) { 152 | suppValuesBNT[field] = unit; 153 | } else { 154 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 155 | }}); 156 | suppValuesValidBNT = true; 157 | } 158 | return suppValuesBNT; 159 | } 160 | } 161 | } 162 | 163 | 164 | get partNo() : number { 165 | if(typeof this._partNo === 'number') { 166 | return this._partNo; 167 | } else { 168 | return this._partNo = this._bitField.getInt(38,2,true) ? 1 : 0; 169 | } 170 | } 171 | 172 | get name() : string { 173 | if(this.partNo === 0) { 174 | return this._formatStr(this._bitField.getString(40,120)); 175 | } else { 176 | return ''; 177 | } 178 | } 179 | 180 | get shipType() : number { 181 | if(this.partNo === 1) { 182 | return this._bitField.getInt(40,8,true); 183 | } else { 184 | return NaN; 185 | } 186 | } 187 | 188 | get callSign() : string { 189 | if(this.partNo === 1) { 190 | return this._formatStr(this._bitField.getString(90,42)); 191 | } else { 192 | return ''; 193 | } 194 | } 195 | 196 | get vendorId() : string { 197 | if(this.partNo === 1) { 198 | return this._formatStr(this._bitField.getString(48,42)); 199 | } else { 200 | return ''; 201 | } 202 | } 203 | 204 | _isTender() : boolean { 205 | if(typeof this._tender !== 'boolean') { 206 | this._tender = String(this.mmsi).startsWith('98'); 207 | } 208 | return this._tender; 209 | } 210 | 211 | _getDimToBow() : number { 212 | if((this.partNo === 1) && !this._isTender()) { 213 | return this._bitField.getInt(132,9,true); 214 | } else { 215 | return NaN; 216 | } 217 | } 218 | 219 | _getDimToStern() : number { 220 | if((this.partNo === 1) && !this._isTender()) { 221 | return this._bitField.getInt(141,9,true); 222 | } else { 223 | return NaN; 224 | } 225 | } 226 | 227 | _getDimToPort() : number { 228 | if((this.partNo === 1) && !this._isTender()) { 229 | return this._bitField.getInt(150,6,true); 230 | } else { 231 | return NaN; 232 | } 233 | } 234 | 235 | _getDimToStbrd() : number { 236 | if((this.partNo === 1) && !this._isTender()) { 237 | return this._bitField.getInt(156,6,true); 238 | } else { 239 | return NaN; 240 | } 241 | } 242 | 243 | get mothershipMmsi() : number { 244 | if((this.partNo === 1) && this._isTender()) { 245 | return this._bitField.getInt(132,30,true); 246 | } else { 247 | return NaN; 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/Ais27Msg.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import type { SuppValues } from './AisMessage'; 23 | 24 | const MOD_NAME = 'Ais27Msg'; 25 | 26 | const SUPPORTED_FIELDS = [ 27 | 'aisType', 28 | 'channel', 29 | 'repeatInd', 30 | 'mmsi', 31 | 'midCountry', 32 | 'midCountryIso', 33 | 'mmsiType', 34 | 'class', 35 | 'navStatus', 36 | 'navStatusStr', 37 | 'sogStatus', 38 | 'sog', 39 | 'cog', 40 | 'latitude', 41 | 'longitude', 42 | 'posAccuracy', 43 | ]; 44 | 45 | let suppValuesValid = false; 46 | let suppValues: SuppValues = {}; 47 | 48 | /* 49 | |============================================================================== 50 | |Field |Len |Description |Member |T|Units 51 | |0-5 | 6 |Message Type |type |u|Constant: 27 52 | |6-7 | 2 |Repeat Indicator |repeat |u|Message repeat count 53 | |8-37 |30 |MMSI |mmsi |u|9 decimal digits 54 | |38-38 | 1 |Position Accuracy |accuracy |u| 55 | |39-39 | 1 |RAIM flag |raim |u| 56 | |40-43 | 4 |Navigation Status |status |e|See "Navigation Status" 57 | |44-61 |18 |Longitude |lon |I4|minutes/10 East positive, West negative 181000 = N/A 58 | |62-78 |17 |Latitude |lat |I4|minutes/10 North positive, South negative 91000 = N/A 59 | |79-84 |6 |Speed Over Ground (SOG) |speed |u|Knots (0-62); 63 = N/A 60 | |85-93 |9 |Course Over Ground (COG)|course |u|0 to 359 degrees, 511 = not available. 61 | |94-94 |1 |GNSS Position status |gnss |u|0 = current GNSS position 1 = not GNSS position (default) 62 | |95-95 |1 |spare | |u|not used 63 | |============================================================================== 64 | 65 | */ 66 | 67 | export default class Ais27Msg extends AisMessage { 68 | constructor(aisType: number, bitField: AisBitField, channel: string) { 69 | super(aisType, bitField, channel); 70 | if (bitField.bits >= 94) { 71 | this._valid = 'VALID'; 72 | } else { 73 | this._valid = 'INVALID'; 74 | this._errMsg = 'invalid bitcount for type CNB msg:' + bitField.bits; 75 | } 76 | } 77 | 78 | get class(): string { 79 | return 'A'; 80 | } 81 | 82 | get supportedValues(): SuppValues { 83 | if (!suppValuesValid) { 84 | SUPPORTED_FIELDS.forEach((field) => { 85 | let unit = AisMessage.getUnit(field); 86 | if (unit) { 87 | suppValues[field] = unit; 88 | } else { 89 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 90 | } 91 | }); 92 | suppValuesValid = true; 93 | } 94 | return suppValues; 95 | } 96 | 97 | get navStatus(): number { 98 | return this._bitField.getInt(40, 4, true); 99 | } 100 | 101 | _getRawSog(): number { 102 | return this._bitField.getInt(79, 6, true) * 10; 103 | } 104 | 105 | _getRawCog(): number { 106 | return this._bitField.getInt(85, 9, true) * 10; 107 | } 108 | 109 | get posAccuracy(): boolean { 110 | return this._bitField.getInt(38, 1, true); 111 | } 112 | 113 | _getRawLat(): number { 114 | return this._bitField.getInt(62, 17, false) * 1000; 115 | } 116 | 117 | _getRawLon(): number { 118 | return this._bitField.getInt(44, 18, false) * 1000; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/AisBitField.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | const DEBUG = false 21 | const MOD_NAME = 'AisBitField'; 22 | const AIS_CHR_TBL : Array = [ 23 | '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 24 | 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', 25 | ' ', '!', '\'', '#', '$', '%', '&', '"', '(', ')', '*', '+', ',', '-', '.', '/', 26 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?' ]; 27 | 28 | 29 | let padStart = function(str : string,tgtLen : number,padStr : string = '0') : string { 30 | let fillLen = tgtLen - str.length; 31 | if(fillLen <= 0) { 32 | return str; 33 | } 34 | 35 | let filler = padStr; 36 | let fLen = filler.length; 37 | while(fLen < fillLen) { 38 | let rem = fillLen - fLen; 39 | filler += fLen > rem ? filler.slice(0, rem) : filler; 40 | fLen = filler.length; 41 | } 42 | if(fLen > fillLen) { 43 | filler = filler.slice(0,fillLen); 44 | } 45 | return filler + str; 46 | } 47 | 48 | var printByte = function(byte : number,bits : number = 8) : string { 49 | return Number(byte & ((1 << bits) - 1)).toString(2).padStart(bits,'0'); 50 | } 51 | 52 | export default class AisBitField { 53 | _aisStr : string; 54 | _strLen : number; 55 | _bytes : Array; 56 | _bits : number; 57 | 58 | constructor(str : string, padBits : number) { 59 | if(DEBUG) console.log(MOD_NAME + '.constructor(' + str + ',' + padBits + ')'); 60 | if(str) { 61 | this._aisStr = str; 62 | let strLen = this._strLen = str.length; 63 | this._bits = strLen * 6; 64 | let len : number = Math.floor(this._bits / 8) + (((this._bits % 8) == 0) ? 0 : 1); 65 | this._bytes = new Array(len); 66 | if(DEBUG) console.log(MOD_NAME + '.constructor() array size:' + this._bytes.length); 67 | if (this._bits > padBits) { 68 | this._bits -= padBits; 69 | } else { 70 | throw(MOD_NAME + ".parse() invalid bitcount encountered:" + (this._bits - padBits)); 71 | } 72 | } else { 73 | this._bits = 0; 74 | } 75 | } 76 | 77 | get bits() : number { 78 | return this._bits; 79 | } 80 | 81 | // return 6-bit 82 | _getByte(idx : number) : number { 83 | let byte : ?number = this._bytes[idx]; 84 | if(typeof byte === 'number') { 85 | return byte; 86 | } else { 87 | let char : number = this._aisStr.charCodeAt(idx); 88 | if(char > 47) { 89 | if(char < 88) { 90 | return this._bytes[idx] = char - 48; 91 | } else { 92 | if(char < 96) { 93 | throw(MOD_NAME + '.parse() invalid character encountered:' + char + ' at index ' + idx); 94 | } else { 95 | if(char < 120) { 96 | return this._bytes[idx] = char - 56; 97 | } else { 98 | throw(MOD_NAME + '.parse() invalid character encountered:' + char + ' at index ' + idx); 99 | } 100 | } 101 | } 102 | } else { 103 | throw(MOD_NAME + '.parse() invalid character encountered:' + char + ' at index ' + idx); 104 | } 105 | } 106 | } 107 | 108 | getInt(start : number,len : number,unsigned : boolean) : number { 109 | if(DEBUG) console.log(MOD_NAME + '.getInt(' + start + ',' + len + ',' + unsigned.toString() + ')'); 110 | 111 | if(len <= 0) { 112 | return 0; 113 | } 114 | 115 | if((len > 31) || ((start + len) > this._bits)) { 116 | throw(MOD_NAME + '.getInt() invalid invalid indexes encountered:' + start + ' ' + len); 117 | } 118 | 119 | //let byteCount : number = Math.floor(len / 6); 120 | let bitIdx : number = start % 6; 121 | let byteIdx : number = Math.floor(start / 6); 122 | let retVal = 0; 123 | 124 | if(DEBUG) console.log(MOD_NAME + '.getInt() bitIdx:' + bitIdx + ' byteIdx:' + byteIdx); 125 | 126 | let i : number; 127 | let bits = 0; 128 | if(bitIdx > 0) { 129 | let rShift : number = 6 - bitIdx; 130 | retVal = this._getByte(byteIdx++) & (0x3F >> bitIdx); 131 | bits = rShift; 132 | } 133 | 134 | let max = Math.min(len,25); 135 | while(bits < max) { 136 | retVal = (retVal << 6) | this._getByte(byteIdx++); 137 | bits += 6; 138 | } 139 | 140 | if(bits > len) { 141 | retVal >>= (bits -len); 142 | } else { 143 | if(bits < len) { 144 | let rest : number = len - bits; 145 | retVal = (retVal << rest) | (this._getByte(byteIdx) >> (6 - rest)); 146 | } 147 | } 148 | 149 | if(!unsigned && (len < 32)) { 150 | let compl : number = (1 << (len - 1)); 151 | if((retVal & compl) != 0) { 152 | // shit, its negative 153 | retVal = (retVal & ~compl) - compl; 154 | } 155 | } 156 | return retVal; 157 | } 158 | 159 | getString(start : number,len : number) : string { 160 | if(((len % 6) != 0) || ((start + len) > this._bits) || (start < 0)) { 161 | throw(MOD_NAME + '.getString() invalid indexes encountered: start:' + start + ' len:' + len + ' bits:' + this._bits); 162 | } 163 | 164 | if(len === 0) { 165 | return ''; 166 | } 167 | 168 | let bitIdx : number = start % 6; 169 | let byteIdx : number = Math.floor(start / 6); 170 | let result : string = ''; 171 | //SysLog.logWarning(this.getClass().getName() + "::getString(" + start + "," + len + ") bitIdx:" + bitIdx + " byteIdx:" + byteIdx); 172 | 173 | if(bitIdx === 0) { 174 | let endIdx : number = byteIdx + len / 6; 175 | while(byteIdx < endIdx) { 176 | result += AIS_CHR_TBL[this._getByte(byteIdx ++)]; 177 | } 178 | } else { 179 | let hiMask : number = ((0x1 << bitIdx) - 1) << (6 - bitIdx); 180 | let loMask : number = ((0x1 << (6 - bitIdx)) - 1); 181 | let endIdx : number = byteIdx + len / 6 + 1; 182 | let chrIdx : number = (this._getByte(byteIdx++) & loMask) << bitIdx; 183 | while(byteIdx < endIdx) { 184 | let byte : number = this._getByte(byteIdx++); 185 | result += AIS_CHR_TBL[chrIdx | (byte & hiMask) >> (6 - bitIdx)]; 186 | chrIdx = (byte & loMask) << bitIdx; 187 | } 188 | } 189 | return result; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/AisCNBMsg.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import type {SuppValues} from './AisMessage'; 23 | const MOD_NAME = 'AisCNBMsg'; 24 | 25 | const SUPPORTED_FIELDS = [ 26 | 'aisType', 27 | 'channel', 28 | 'repeatInd', 29 | 'mmsi', 30 | 'midCountry', 31 | 'midCountryIso', 32 | 'mmsiType', 33 | 'class', 34 | 'navStatus', 35 | 'navStatusStr', 36 | 'rotStatus', 37 | 'rot', 38 | 'heading', 39 | 'sogStatus', 40 | 'sog', 41 | 'cog', 42 | 'latitude', 43 | 'longitude', 44 | 'posAccuracy', 45 | 'utcTsSec', 46 | 'utcTsStatus' 47 | ] 48 | 49 | let suppValuesValid = false; 50 | let suppValues : SuppValues = {}; 51 | 52 | /* 53 | |============================================================================== 54 | |Field |Len |Description |Member |T|Units 55 | |0-5 | 6 |Message Type |type |u|Constant: 1-3 56 | |6-7 | 2 |Repeat Indicator |repeat |u|Message repeat count 57 | |8-37 |30 |MMSI |mmsi |u|9 decimal digits 58 | |38-41 | 4 |Navigation Status |status |e|See "Navigation Status" 59 | |42-49 | 8 |Rate of Turn (ROT) |turn |I3|See below 60 | |50-59 |10 |Speed Over Ground (SOG) |speed |U1|See below 61 | |60-60 | 1 |Position Accuracy |accuracy |b|See below 62 | |61-88 |28 |Longitude |lon |I4|Minutes/10000 (see below) 63 | |89-115 |27 |Latitude |lat |I4|Minutes/10000 (see below) 64 | |116-127 |12 |Course Over Ground (COG)|course |U1|Relative to true north, 65 | to 0.1 degree precision 66 | |128-136 | 9 |True Heading (HDG) |heading |u|0 to 359 degrees, 67 | 511 = not available. 68 | |137-142 | 6 |Time Stamp |second |u|Second of UTC timestamp 69 | TODO: 70 | |143-144 | 2 |Maneuver Indicator |maneuver |e|See "Maneuver Indicator" 71 | |145-147 | 3 |Spare | |x|Not used 72 | |148-148 | 1 |RAIM flag |raim |b|See below 73 | |149-167 |19 |Radio status |radio |u|See below 74 | |============================================================================== 75 | 76 | */ 77 | 78 | export default class AisCNBMsg extends AisMessage { 79 | constructor(aisType : number,bitField : AisBitField, channel : string) { 80 | super(aisType,bitField,channel); 81 | if(bitField.bits >= 144) { 82 | this._valid = 'VALID'; 83 | } else { 84 | this._valid = 'INVALID'; 85 | this._errMsg = 'invalid bitcount for type CNB msg:' + bitField.bits; 86 | } 87 | } 88 | 89 | get class() : string { 90 | return 'A'; 91 | } 92 | 93 | get supportedValues() : SuppValues { 94 | if(!suppValuesValid) { 95 | SUPPORTED_FIELDS.forEach((field)=>{ 96 | let unit = AisMessage.getUnit(field); 97 | if(unit) { 98 | suppValues[field] = unit; 99 | } else { 100 | console.warn(MOD_NAME + 'field without unit encountered:' + field); 101 | }}); 102 | suppValuesValid = true; 103 | } 104 | return suppValues; 105 | } 106 | 107 | get navStatus() : number { 108 | return this._bitField.getInt(38,4,true); 109 | } 110 | 111 | _getRawRot() : number { 112 | return this._bitField.getInt(42,8,false); 113 | } 114 | 115 | _getRawHeading() : number { 116 | return this._bitField.getInt(128, 9, true); 117 | } 118 | 119 | _getRawSog() : number { 120 | return this._bitField.getInt(50, 10, true); 121 | } 122 | 123 | _getRawCog() : number { 124 | return this._bitField.getInt(116, 12, true); 125 | } 126 | 127 | get posAccuracy() : boolean { 128 | return this._bitField.getInt(60, 1, true) === 1; 129 | } 130 | 131 | _getUtcSec() : number { 132 | return this._bitField.getInt(137,6,true); 133 | } 134 | 135 | _getRawLat() : number { 136 | return this._bitField.getInt(89,27,false); 137 | } 138 | 139 | _getRawLon() : number { 140 | return this._bitField.getInt(61,28,false); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/AisParser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* 4 | * AisParser: A parser for NMEA0183 AIS messages. 5 | * Copyright (C) 2017 Thomas Runte . 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the Apache License Version 2.0 as published by 9 | * Apache Software foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the Apache License Version 2.0 17 | * along with this program. If not, see . 18 | */ 19 | 20 | import AisBitField from './AisBitField'; 21 | import AisMessage from './AisMessage'; 22 | import AisCNBMsg from './AisCNBMsg'; 23 | import Ais04Msg from './Ais04Msg'; 24 | import Ais05Msg from './Ais05Msg'; 25 | import Ais18Msg from './Ais18Msg'; 26 | import Ais19Msg from './Ais19Msg'; 27 | import Ais21Msg from './Ais21Msg'; 28 | import Ais24Msg from './Ais24Msg'; 29 | import Ais27Msg from './Ais27Msg'; 30 | 31 | // TODO: Parser currently rejects multipart messages, if the padbit is != 0 in 32 | // any but the last part. In an AisHub scan 2 messages where encountered 33 | // that where built like that but they were invalid in other ways too, 34 | // so I am hoping to get away like this. 35 | 36 | export type ParseOptions = { checksum? : boolean; }; 37 | 38 | export type Context = { [id : string] : { idx : number, aisStr: string } }; 39 | 40 | const MOD_NAME = 'AisParser'; 41 | const DEBUG = false 42 | 43 | class AisParser { 44 | _context : Context; 45 | _options : ParseOptions; 46 | 47 | constructor(options : ParseOptions = {}) { 48 | this._options = options; 49 | this._context = {}; 50 | } 51 | 52 | static checksumValid(sentence : string) : boolean { 53 | if(!(sentence.startsWith('!AIVDO') || sentence.startsWith('!AIVDM'))) { 54 | return false; 55 | } 56 | 57 | let idx = sentence.indexOf('*'); 58 | if((idx === -1) || (idx < 2)) { 59 | return false; 60 | } 61 | 62 | let len : number = idx - 1; 63 | let chkSum : number = 0; 64 | let i : number; 65 | if(DEBUG) console.log(MOD_NAME + '.checksumValid(' + sentence + ') on ' + sentence.substr(1,len)); 66 | for(i = 1;i < idx;i++) { 67 | // if(DEBUG) console.log(MOD_NAME + '.checksumValid() index:' + i + ' value:' + strBuf.readUInt8(i)); 68 | chkSum ^= sentence.charCodeAt(i) & 0xFF; 69 | } 70 | 71 | let chkSumStr : string = chkSum.toString(16).toUpperCase(); 72 | if(chkSumStr.length < 2) { 73 | chkSumStr = '0' + chkSumStr; 74 | } 75 | if(DEBUG && (chkSumStr !== sentence.substr(idx + 1))) { 76 | console.warn(MOD_NAME + '.checksumValid(' + sentence + ') ' + chkSumStr+ '!==' + sentence.substr(idx + 1)); 77 | } 78 | return chkSumStr === sentence.substr(idx + 1); 79 | } 80 | 81 | parse(sentence : string,options : ParseOptions = {}) : AisMessage { 82 | let checksum = (typeof options.checksum !== 'undefined') ? options.checksum : this._options.checksum; 83 | if(checksum && !AisParser.checksumValid(sentence)) { 84 | return AisMessage.fromError('INVALID','Invalid checksum in message: [' + sentence + ']'); 85 | } 86 | return this.parseArray(sentence.split(',')) 87 | } 88 | 89 | // !AIVDM,1,1,,B,14`c;d002grD>PH50hr7RVE000SG,0*74 90 | parseArray(part : Array) : AisMessage { 91 | let parts : number = part.length 92 | 93 | if(parts !== 7) { 94 | return AisMessage.fromError('INVALID','Invalid count (!=7) of comma separated elements in message: [' + String(part) + ']'); 95 | } else { 96 | if((part[0] !== '!AIVDM') && (part[0] !== '!AIVDO')) { 97 | return AisMessage.fromError('UNSUPPORTED','not a supported AIS message:[' + String(part) + ']'); 98 | } 99 | } 100 | 101 | let msgCount : number = Number(part[1]); 102 | let msgIdx : number = Number(part[2]); 103 | let msgId : string = part[3]; 104 | let padBit : number = Number(part[6].substr(0,1)); 105 | let aisStr : string = part[5]; 106 | 107 | if(msgCount > 1) { 108 | if(msgIdx === msgCount) { 109 | let msgParts = this._context[msgId]; 110 | if(!msgParts) { 111 | return AisMessage.fromError('INVALID','missing prior message(s) in partial message:[' + String(part) + ']'); 112 | } 113 | if(msgIdx !== (msgParts.idx + 1)) { 114 | delete this._context[msgId]; 115 | return AisMessage.fromError('INVALID','sequence violation (skipped or missing message) in partial message:[' + String(part) + ']'); 116 | } 117 | aisStr = msgParts.aisStr + aisStr; 118 | delete this._context[msgId]; 119 | } else { 120 | if(padBit !== 0) { 121 | return AisMessage.fromError('UNSUPPORTED','padbit!=0 not supported in partial message:[' + String(part) + ']'); 122 | } 123 | let msgParts = this._context[msgId]; 124 | if(msgIdx === 1) { 125 | if(typeof msgParts !== 'undefined') { 126 | delete this._context[msgId]; 127 | return AisMessage.fromError('INVALID','a message with this sequence and index already exists in partial message:[' + String(part) + ']'); 128 | } 129 | this._context[msgId] = { idx : msgIdx, aisStr: aisStr }; 130 | return AisMessage.fromError('INCOMPLETE',''); 131 | } else { 132 | if(!msgParts) { 133 | return AisMessage.fromError('INVALID','missing prior message in partial message:[' + String(part) + ']'); 134 | } 135 | if(msgIdx !== (msgParts.idx + 1)) { 136 | delete this._context[msgId]; 137 | return AisMessage.fromError('INVALID','sequence violation (skipped or missing message) in partial message:[' + String(part) + ']'); 138 | } 139 | msgParts.idx = msgIdx; 140 | msgParts.aisStr += aisStr; 141 | return AisMessage.fromError('INCOMPLETE',''); 142 | } 143 | } 144 | } else { 145 | if(msgIdx !== 1) { 146 | return AisMessage.fromError('INVALID','invalid message index !=1 in non partial message:[' + String(part) + ']'); 147 | } 148 | } 149 | 150 | try { 151 | let bitField : AisBitField = new AisBitField(aisStr,padBit); 152 | let aisType : number = bitField.getInt(0,6,true); 153 | switch(aisType) { 154 | case 1: 155 | case 2: 156 | case 3: 157 | return new AisCNBMsg(aisType,bitField,part[4]); 158 | case 4: 159 | return new Ais04Msg(aisType,bitField,part[4]); 160 | case 5: 161 | return new Ais05Msg(aisType,bitField,part[4]); 162 | case 18: 163 | return new Ais18Msg(aisType,bitField,part[4]); 164 | case 19: 165 | return new Ais19Msg(aisType,bitField,part[4]); 166 | case 21: 167 | return new Ais21Msg(aisType,bitField,part[4]); 168 | case 24: 169 | return new Ais24Msg(aisType,bitField,part[4]); 170 | case 27: 171 | return new Ais27Msg(aisType,bitField,part[4]); 172 | default: 173 | return AisMessage.fromError( 174 | 'UNSUPPORTED', 175 | 'Unsupported ais type ' + aisType + ' in message [' + String(part) + ']', 176 | aisType, 177 | part[4]); 178 | } 179 | } catch(error) { 180 | return AisMessage.fromError('INVALID','Failed to parse message, error:' + error); 181 | } 182 | } 183 | } 184 | module.exports = AisParser; 185 | -------------------------------------------------------------------------------- /src/__tests__/AisBitFieldGetInt.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import AisBitField from '../AisBitField' 4 | import {isValidString,createInvalidStringTestData,createIntTestData} from './testHelper/AisBitfieldDataGenerator' 5 | 6 | 7 | //const invalid : TestData = createInvalidStringTestData() 8 | // const params : TestData = createStringTestData() 9 | let curr : TestData = createIntTestData() 10 | 11 | let idx : number = 0 12 | for(;idx < 50;idx ++) { 13 | test('test AisBitfield.getInt with: {' + curr.aisStr + '} padBits:' + curr.padBits + ' bits:' + curr.bits + ' start:' + curr.start + ' numBits:' + curr.numBits, () => { 14 | expect(curr).toBeDefined() 15 | let bf: AisBitField = new AisBitField(curr.aisStr, curr.padBits) 16 | expect(bf).toBeDefined() 17 | if (bf) { 18 | let num: ?number = bf.getInt(curr.start, curr.numBits,true) 19 | expect(num).toBeDefined() 20 | expect(num >= 0).toBeTruthy() 21 | } 22 | }) 23 | 24 | curr = createIntTestData() 25 | } -------------------------------------------------------------------------------- /src/__tests__/AisBitFieldGetString.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | import AisBitField from '../AisBitField' 3 | import {isValidString,createInvalidStringTestData,createStringTestData} from './testHelper/AisBitfieldDataGenerator' 4 | 5 | 6 | const invalid : TestData = createInvalidStringTestData() 7 | const params : TestData = createStringTestData() 8 | let curr : TestData = createStringTestData() 9 | 10 | let idx : number = 0 11 | for(;idx < 50;idx ++) { 12 | test('test AisBitfield.getString with: {' + curr.aisStr + '} padBits:' + curr.padBits + ' bits:' + curr.bits + ' start:' + curr.start + ' numBits:' + curr.numBits, () => { 13 | expect(curr).toBeDefined() 14 | let bf: AisBitField = new AisBitField(curr.aisStr, curr.padBits) 15 | expect(bf).toBeDefined() 16 | if (bf) { 17 | let str: ?string = bf.getString(curr.start, curr.numBits) 18 | expect(str).toBeDefined() 19 | expect(isValidString(str)).toBe('') 20 | expect(str.length).toBe(curr.numBits / 6) 21 | } 22 | }) 23 | 24 | curr = createStringTestData() 25 | } 26 | 27 | test('Test with invalid params on {' + params.aisStr + '} padBits:' + params.padBits + ' bits:' + params.bits + ' start: -1 numBits: 12',()=> { 28 | let bf: AisBitField = new AisBitField(params.aisStr, params.padBits) 29 | 30 | expect(bf).toBeDefined() 31 | 32 | function invalid0() { 33 | bf.getString(-1, 12) 34 | } 35 | 36 | expect(invalid0).toThrow(/invalid indexes encountered/); 37 | }) 38 | 39 | test('Test with invalid params on {' + params.aisStr + '} padBits:' + params.padBits + ' bits:' + params.bits + ' start: 0 numBits: ' + ((Math.floor(params.bits / 6) + 1) * 6),()=> { 40 | let bf: AisBitField = new AisBitField(params.aisStr, params.padBits) 41 | 42 | expect(bf).toBeDefined() 43 | 44 | function invalid0() { 45 | bf.getString(0, (Math.floor(params.bits / 6) + 1) * 6) 46 | } 47 | 48 | expect(invalid0).toThrow(/invalid indexes encountered/); 49 | }) 50 | 51 | 52 | test('Test with invalid params on {' + params.aisStr + '} padBits:' + params.padBits + ' bits:' + params.bits + ' start: 7 numBits: ' + (Math.floor(params.bits / 6) * 6),()=> { 53 | let bf: AisBitField = new AisBitField(params.aisStr, params.padBits) 54 | 55 | expect(bf).toBeDefined() 56 | 57 | function invalid0() { 58 | bf.getString(7, Math.floor(params.bits / 6) * 6) 59 | } 60 | 61 | expect(invalid0).toThrow(/invalid indexes encountered/); 62 | }) 63 | 64 | test('Test with invalid params on {' + params.aisStr + '} padBits:' + params.padBits + ' bits:' + params.bits + ' start: 4 numBits: 11',()=> { 65 | let bf: AisBitField = new AisBitField(params.aisStr, params.padBits) 66 | 67 | expect(bf).toBeDefined() 68 | 69 | function invalid0() { 70 | bf.getString(4,11) 71 | } 72 | 73 | expect(invalid0).toThrow(/invalid indexes encountered/); 74 | }) 75 | 76 | 77 | test('Test with invalid chars on {' + invalid.aisStr + '} padBits:' + invalid.padBits + ' bits:' + invalid.bits + ' start: ' + invalid.start + ' numBits: ' + invalid.numBits,()=>{ 78 | expect(invalid).toBeDefined() 79 | let bf: AisBitField = new AisBitField(invalid.aisStr, invalid.padBits) 80 | expect(bf).toBeDefined() 81 | function invalid0() { 82 | bf.getString(invalid.start,invalid.numBits) 83 | } 84 | expect(invalid0).toThrow(/invalid character encountered/) 85 | }) -------------------------------------------------------------------------------- /src/__tests__/AisParserTest.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import AisParser from '../AisParser' 4 | import {getRealAisData} from './testHelper/AisBitfieldDataGenerator' 5 | import type RealTestData from './testHelper/AisBitfieldDataGenerator' 6 | import AisMessage from "../AisMessage" 7 | 8 | 9 | let rad: ?RealTestData 10 | test('testing real data ', () => { 11 | let parser = new AisParser({checksum: true}) 12 | expect(parser).toBeDefined() 13 | let idx: number = 0 14 | while (rad = getRealAisData()) { 15 | let msg: AisMessage = parser.parse(rad.aisStr) 16 | expect(msg).toBeDefined() 17 | expect(msg.valid).toBe('VALID') 18 | expect(msg.aisType).toBe(rad.aisType) 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/__tests__/testHelper/AisBitfieldDataGenerator.js: -------------------------------------------------------------------------------- 1 | import RandSeed from "random-seed" 2 | 3 | const AIS_OUT_CHR_TBL: Array = [ 4 | '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 5 | 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', 6 | ' ', '!', '\'', '#', '$', '%', '&', '"', '(', ')', '*', '+', ',', '-', '.', '/', 7 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?'] 8 | 9 | const AIS_IN_CHR_TBL: Array = [ 10 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', 11 | '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 12 | 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 13 | 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'] 14 | 15 | 16 | export type TestData = { 17 | aisStr: string, 18 | padBits: number, 19 | bits: number, 20 | start: number, 21 | numBits: number 22 | } 23 | 24 | export type RealData = { 25 | aisStr: string, 26 | channel : string, 27 | aisType : number, 28 | valid : boolean 29 | } 30 | 31 | const AIS_REAL_DATA : Array = [ 32 | { aisStr : '!AIVDM,1,1,,B,14`c;d002grD>PH50hr7RVE000SG,0*74', 33 | channel : 'B', 34 | aisType : 1, 35 | valid: true }, 36 | { aisStr : '!AIVDM,1,1,,B,34hwN60Oh3rCwib56`qJtbL<0000,0*12', 37 | channel : 'B', 38 | aisType : 3, 39 | valid: true } 40 | ] 41 | 42 | /* 43 | 44 | !AIVDM,1,1,,B,34hwN60Oh3rCwib56`qJtbL<0000,0*12 45 | !AIVDM,1,1,,B,15TPq@0Oj0rClEv53P9HWVn<283C,0*51 46 | !AIVDO,1,1,,,B39i>1000nTu;gQAlBj:wwS5kP06,0*5D 47 | !AIVDM,1,1,,A,35SrP>5P00rCQAL5:KA8b0000rCgTH58DU6KpJj0`0>,0*37 51 | !AIVDO,1,1,,,B39i>1001FTu;bQAlAMscwe5kP06,0*3E 52 | !AIVDM,1,1,,B,15Bs:H8001JCUE852dB2p42@0D7UKSOCR1A84q<10006Tu;PQAl?uoWwUUkP06,0*43 60 | !AIVDM,1,1,,A,15AfoT?002rCpob54FmCFP@H2<4C,0*71 61 | !AIVDM,1,1,,B,15ATk20001JCqnl54fmTt1:@0<0l,0*27 62 | !AIVDM,1,1,,B,15>kSB8P00rC``<59josGOvF2@6b,0*64 63 | !AIVDM,1,1,,B,15RovJ0P02rCttP56n@Db?vH26C8,0*56 64 | !AIVDM,1,1,,B,803Iu6PF1d=h0a@QAa>E;@30JcCsivKTvtOVq?g7q0`1wwqLD0,4*20 65 | !AIVDO,1,1,,,B39i>10006Tu;PQAl?tTowV5kP06,0*22 66 | !AIVDM,1,1,,B,3E>o=b?P2KJB;NP5FMrmAwv80000,0*3F 67 | !AIVDM,1,1,,A,15D>NJ0000rCoub53sq5tPRF2D3v,0*27 68 | !AIVDM,1,1,,B,14hwN60Oh1rCwh`56`tK>02H00Rs,0*26 69 | !AIVDM,1,1,,A,35@Cc`0001rCP585:OBenrjF001S,0*78 70 | !AIVDM,1,1,,A,18157t0OhSrCv?B53PHt;bnJ00Sb,0*30 71 | !AIVDM,1,1,,B,1000Fo@P01rCuDL56cMMegvH0@7E,0*0E 72 | !AIVDO,1,1,,,B39i>1000FTu;PQAl?tMKwVUkP06,0*0F 73 | !AIVDM,1,1,,B,15`OGN?01lrDhFP51=hdS:2J287a,0*65 74 | !AIVDO,1,1,,,B39i>1000VTu;PQAl?tM7wW5kP06,0*02 75 | !AIVDM,1,1,,B,109t5aW000rCi@v57foqoP4L2H8;,0*70 76 | !AIVDM,1,1,,A,A03IupkAC4:dH0v9WJhP1cF6nud@NrP5wH5T,0*23 77 | !AIVDM,1,1,,A,15R=@2:P01rCTfL52hhJmgvL20SV,0*61 78 | !AIVDM,1,1,,B,15QDvN0P00rC`Rf59jOQlOvJ2H8D,0*68 79 | !AIVDM,1,1,,B,35AbuT1P01rD1at55VqDWwvL0000,0*2A 80 | !AIVDO,1,1,,,B39i>1000nTu;Q1Al@0RswWUkP06,0*5B 81 | !AIVDM,1,1,,B,15?Jc<0004rCswh537mTlVLL088b,0*32 82 | !AIVDM,1,1,,A,15TPq@0PB1rCkS:53HqHeVlN2<4L,0*27 83 | !AIVDM,1,1,,A,15?P>b0000rCgTd58DU6KpJL0PRR,0*00 84 | !AIVDM,1,1,,A,15TPmF5OisrCjR8537v8woNN2H8k,0*12 85 | !AIVDM,1,1,,A,14`a`N001VrD0jt4sHdWm68P2@8v,0*40 86 | !AIVDM,1,1,,A,15NQaV?P01rCu1456IlbTOvP2<4a,0*07 87 | !AIVDM,1,1,,B,H5>gpV0@tp60hT<60T000000000,2*35 88 | !AIVDM,1,1,,B,3E>o=b?P2KJB;NP5FMrmAwv80000,0*3F 89 | !AIVDO,1,1,,,B39i>1000nTu;Q1Al@0T?w`5kP06,0*46 90 | !AIVDM,1,1,,B,13aA4?wp00Rk,0*20 92 | !AIVDM,1,1,,A,15Bs:H8001JCUDh52dDKT54o0LCSVP2Dnb,0*06 99 | !AIVDM,1,1,,A,35RK:b1000rCqh@540@8Q06N0000,0*58 100 | !AIVDM,1,1,,A,1ENQkw0000rB13v5G4hpoQ2F0000,0*01 101 | !AIVDM,2,1,9,B,55ADDF2h=<`0PvoC;OHntr0@T4ltpB18UHE:22165h55<75c07PPC0ShH1F4,0*5F 102 | !AIVDM,2,2,9,B,33ljEQH8880,2*4C 103 | !AIVDM,1,1,,A,11aucipP02rD0t`56SAaq?vR2@9m,0*34 104 | !AIVDM,1,1,,A,14`WDl?000JCi:J57nTo8IbR0D0`,0*11 105 | !AIVDO,1,1,,,B39i>10016Tu;QQAl@4LWw`UkP06,0*6B 106 | !AIVDM,1,1,,A,39NSD0000,0*78 110 | !AIVDM,1,1,,B,15NMBi;P00rCfh<58C3s??vT2D4J,0*45 111 | !AIVDM,1,1,,B,H5>gpV4m71B=9>08@pqjh00P9220,0*12 112 | !AIVDM,1,1,,A,15NK0G0P01rCl6F57Am9jgvR08:>,0*2E 113 | !AIVDM,1,1,,B,15RcEl001cJCshH4u=u0bPbL0<2B,0*68 114 | !AIVDO,1,1,,,B39i>1000nTu;I1Al>tgOwi5kP06,0*2E 115 | !AIVDM,1,1,,A,1775gb0000rCaut59P?PB9o405pT,0*44 116 | !AIVDM,1,1,,B,35BDU`0PAurCL1L5:oS4sSc400uQ,0*10 117 | !AIVDM,1,1,,A,15R=@2:P00rCplb53r6sU?w625pT,0*23 118 | !AIVDM,1,1,,B,15>uP00P0grCWsb59gOLG?w420Sa,0*29 119 | !AIVDM,1,1,,B,15`OGN?Oh2rCww456Rqa8hO62@DA,0*37 120 | !AIVDM,1,1,,A,15QdFT?P00JCr7454CD2eww40D3H,0*78 121 | !AIVDO,1,1,,,B39i>1000nTu;IQAl?0cowiUkP06,0*4F 122 | !AIVDM,1,1,,B,14a1Eb01hErCdBj59;:,0*50 125 | !AIVDM,2,1,4,B,A03IupkAC4:dH0N97cep6Oi30EDOw3?v6ASsQ09C4goA03dwG<0>QKtqh0c,0*21 127 | !AIVDM,1,1,,B,15QDCP0P?w4?wp073h,0*45 128 | !AIVDM,1,1,,A,3EU`;h?P1PJCnA053<;e:gvp2000,0*13 129 | !AIVDM,1,1,,A,35TPq@0000rCfV457oNQ?GG625hS,0*67 130 | !AIVDM,1,1,,A,15NQaV?P@LrCdv0591FdV:U82@Dd,0*36 131 | !AIVDO,1,1,,,B39i>1000nTu;J1Al?4b3wj5kP06,0*16 132 | !AIVDM,1,1,,B,8H152bPF0P000000000004QFADh000000>hi20000h0,2*5E 133 | !AIVDM,1,1,,B,15D>NJ0000rCp7253rwpU09625pT,0*21 134 | !AIVDM,1,1,,A,15RVtT0P00rCl9>533ES@gw82<1R,0*1A 135 | !AIVDM,2,1,5,B,5INWsv@2>aG<7PmSB20<520M8DLu9V2222222217HA6;E4wG0J@k5PD?T0@S,0*2B 136 | !AIVDM,2,2,5,B,0`888888880,2*72 137 | !AIVDM,1,1,,A,15Bs:H8001JCpnv53qwkTP1p2hEA,0*79 138 | !AIVDM,1,1,,A,15SNCR8P0eJC`B<59cRtU?w625pT,0*45 139 | !AIVDM,1,1,,B,15QK90?000JD1;D54SlAfh5p0Up@,0*1C 140 | !AIVDO,1,1,,,B39i>10016Tu;JQAl?4UgwjUkP06,0*2C 141 | !AIVDM,1,1,,B,15?Jc<0001rCw8R53jkQg:o808EL,0*7B 142 | !AIVDM,1,1,,B,15NMBi;P00rCfi:58C3CN?w:2D4F,0*60 143 | !AIVDM,1,1,,A,11aucipP02rD14R56PU:hww:2<4V,0*79 144 | */ 145 | 146 | 147 | let seed: number = 47110815 148 | let rand: RandSeed = new RandSeed(seed) 149 | let realDataIdx = 0 150 | 151 | 152 | 153 | function isValidString(str: string): string { 154 | let idx: number = 0 155 | for (; idx < str.length; idx++) { 156 | if (AIS_OUT_CHR_TBL.indexOf(str.charAt(idx)) < 0) { 157 | console.log() 158 | return 'invalid character ' + str.charAt(idx) + ' at index ' + idx 159 | } 160 | } 161 | 162 | return '' 163 | } 164 | 165 | function createTestData(): TestData { 166 | // enough bits to read a 32 bit integer or a 6 character string - 7 * 6 - 5 = 37 bits minimum 167 | let count = rand.intBetween(7, 100) 168 | let tmp : string = '' 169 | let idx: number = 0 170 | for (; idx < count; idx++) { 171 | // one of the legal characters 172 | tmp += AIS_IN_CHR_TBL[rand.intBetween(0, AIS_IN_CHR_TBL.length - 1)] 173 | } 174 | 175 | let padBits: number = rand.intBetween(0, 5) 176 | 177 | return { 178 | aisStr: tmp, 179 | padBits: padBits, 180 | bits: count * 6 - padBits, 181 | start: 0, 182 | numBits: 0 183 | } 184 | } 185 | 186 | 187 | function createStringTestData(): TestData { 188 | let result : TestData = createTestData() 189 | result.start = rand.intBetween(0, result.bits - 6) 190 | result.numBits = rand.intBetween(0, (result.bits - result.start) / 6) * 6 191 | return result 192 | } 193 | 194 | function createInvalidStringTestData(): TestData { 195 | let result : TestData = createTestData() 196 | 197 | let failIdx = rand.intBetween(0,result.aisStr.length - 1) 198 | result.numBits = rand.intBetween(1,result.aisStr.length - 1) * 6 199 | result.aisStr = result.aisStr.substr(0, failIdx) + 'X' + result.aisStr.substr(failIdx + 1) 200 | 201 | let stMin = failIdx * 6 - result.numBits - 1 202 | if(stMin < 0) { 203 | stMin = 0 204 | } 205 | 206 | let stMax = failIdx * 6 - 1 207 | 208 | result.start = rand.intBetween(stMin,stMax) 209 | if((result.start + result.numBits) >= result.bits) { 210 | result.start = result.bits - result.numBits 211 | } 212 | 213 | return result 214 | } 215 | 216 | function createIntTestData(): TestData { 217 | let result : TestData = createTestData() 218 | result.numBits = rand.intBetween(1, 32) 219 | result.start = rand.intBetween(0, result.bits - result.numBits) 220 | return result 221 | } 222 | 223 | function getRealAisData() : ?RealData { 224 | if(realDataIdx < AIS_REAL_DATA.length) { 225 | return AIS_REAL_DATA[realDataIdx++] 226 | } 227 | } 228 | 229 | 230 | module.exports = { 231 | isValidString: isValidString, 232 | createStringTestData: createStringTestData, 233 | createInvalidStringTestData: createInvalidStringTestData, 234 | createIntTestData: createIntTestData, 235 | getRealAisData : getRealAisData 236 | } -------------------------------------------------------------------------------- /test.log: -------------------------------------------------------------------------------- 1 | 2 | > aisparser@0.0.13 test /home/thomas/develop/nodejs/AisParser 3 | > jest 4 | 5 | FAIL src/__tests__/AisBitField.js 6 | ● Test suite failed to run 7 | 8 | ReferenceError: idx is not defined 9 | 10 | 57 | let idx1: number = 0 11 | 58 | let max: number = (Math.floor(testData.bits / 6) - 1) * 6 12 | > 59 | for (; idx1 < max; idx1++) { 13 | 60 | //console.log(' start at: ' + idx1) 14 | 61 | let maxChars: number = Math.floor((testData.bits - idx1) / 6) 15 | 62 | let idx2: number = 0 16 | 17 | at Object. (src/__tests__/AisBitField.js:59:35) 18 | 19 | Test Suites: 1 failed, 1 total 20 | Tests: 0 total 21 | Snapshots: 0 total 22 | Time: 1.302s 23 | Ran all test suites. 24 | npm ERR! code ELIFECYCLE 25 | npm ERR! errno 1 26 | npm ERR! aisparser@0.0.13 test: `jest` 27 | npm ERR! Exit status 1 28 | npm ERR! 29 | npm ERR! Failed at the aisparser@0.0.13 test script. 30 | npm ERR! This is probably not a problem with npm. There is likely additional logging output above. 31 | 32 | npm ERR! A complete log of this run can be found in: 33 | npm ERR! /home/thomas/.npm/_logs/2018-04-11T18_53_49_010Z-debug.log 34 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | !*.js 2 | !testdata.tgz 3 | * 4 | 5 | -------------------------------------------------------------------------------- /test/scanFile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | // @flow 3 | 4 | /* 5 | * AisParser: A parser for NMEA0183 AIS messages. 6 | * Copyright (C) 2017 Thomas Runte . 7 | * 8 | * This program is free software: you can redistribute it and/or modify 9 | * it under the terms of the Apache License Version 2.0 as published by 10 | * Apache Software foundation. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the Apache License Version 2.0 18 | * along with this program. If not, see . 19 | */ 20 | 21 | import fs from 'fs'; 22 | import {Transform} from 'stream'; 23 | 24 | import AisMessage from '../src/AisMessage'; 25 | import type {SuppValues} from '../src/AisMessage'; 26 | import AisParser from '../src/AisParser'; 27 | 28 | const TMP_FILE_NAME = 'tmp.out'; 29 | const MOD_NAME = 'scanFile'; 30 | const DEBUG = false; 31 | 32 | class Parser extends Transform { 33 | _fields: { [name : string] : number }; 34 | _fieldCount : number; 35 | _fieldNames : Array; 36 | _template : Array; 37 | _aisContext: Object; 38 | _lineCount : number; 39 | _aisCount : number; 40 | _failCount : number; 41 | _failedMsgs : { [tag:string] : number }; 42 | _oStream : Object; 43 | _iStream : Object; 44 | _failStream : Object; 45 | _lineCache : { [id : string] : Array }; 46 | _typeSigk : boolean; 47 | _ggDuration : number; 48 | _parseDuration : number; 49 | _readDuration : number; 50 | _remaining : string; 51 | _aisParser : AisParser; 52 | 53 | constructor(failStream : Object,sigkType : boolean) { 54 | super(); 55 | this._aisParser = new AisParser({checksum : true}); 56 | this._fields = { 'lineNo': 0 }; 57 | this._fieldCount = 1; 58 | this._fieldNames = ['lineNo']; 59 | this._template = []; 60 | this._aisContext = {}; 61 | this._lineCount = 0; 62 | this._aisCount = 0; 63 | this._failCount = 0; 64 | this._failedMsgs = {}; 65 | this._lineCache = {}; 66 | this._parseDuration = 0; 67 | this._readDuration = 0; 68 | this._remaining = ''; 69 | this._failStream = failStream; 70 | this._typeSigk = sigkType; 71 | } 72 | 73 | _transform(chunk, encoding, callback) { 74 | if(DEBUG) console.log(MOD_NAME + '._transform()'); 75 | this._remaining += chunk.toString(); 76 | let line : Array = this._remaining.split(/\r?\n/); 77 | let lines : number = line.length; 78 | if(lines > 0) { 79 | this._remaining = line.pop(); 80 | line.forEach((curr)=>{ 81 | let output = this.processNMEALine(curr,++this._lineCount); 82 | if(output) this.push(output); 83 | }); 84 | } 85 | callback(); 86 | } 87 | 88 | format(input : string,last : boolean = false) : string { 89 | let term : string = (last ? '\n' : ','); 90 | if(typeof input === 'number') { 91 | if(isNaN(input)) { 92 | return '"N/A"' + term 93 | } else { 94 | return (Number.isInteger(input) ? input : input.toFixed(2)) + term; 95 | } 96 | } else { 97 | if(typeof input !== 'undefined') { 98 | return '"' + String(input) + '"' + term; 99 | } else { 100 | return term; 101 | } 102 | } 103 | } 104 | 105 | arrayToCsv(output : Array) : string { 106 | let outputStr : string = ''; 107 | let i : number; 108 | let curr : any; 109 | for(i = 0;i < (output.length - 1);i ++) { 110 | outputStr += this.format(output[i]); 111 | } 112 | return outputStr + this.format(output[output.length - 1],true); 113 | } 114 | 115 | processNMEALine(rawLine: string,lineNo : number) : ?string { 116 | let line : string; 117 | if(this._typeSigk) { 118 | let idx = rawLine.indexOf(';'); 119 | if(idx === -1) { 120 | console.warn('invalid sigk line:' + lineNo + ' line:' + rawLine); 121 | return; 122 | } 123 | 124 | idx = rawLine.indexOf(';',idx + 1); 125 | if(idx === -1) { 126 | console.warn('invalid sigk line:' + lineNo + ' line:' + rawLine); 127 | return; 128 | } 129 | 130 | line = rawLine.substr(idx + 1); 131 | } else { 132 | line = rawLine; 133 | } 134 | 135 | if(line && line.startsWith('!AI')) { 136 | if(line.length < 10) { 137 | console.warn('line too short:' + lineNo + ' line:' + rawLine); 138 | return; 139 | } 140 | 141 | this._aisCount ++; 142 | 143 | let part : Array = line.split(','); 144 | let msgCount : number = Number(part[1]); 145 | let msgIdx : number = Number(part[2]); 146 | let msgId : string = part[3]; 147 | if(msgCount > 1) { 148 | if(msgIdx === 1) { 149 | this._lineCache[msgId] = [line]; 150 | } else { 151 | this._lineCache[msgId].push(line); 152 | } 153 | } 154 | 155 | let msg : AisMessage; 156 | let keysPrinted : boolean = false; 157 | try { 158 | let startTime = process.hrtime(); 159 | msg = this._aisParser.parse(line); 160 | let dur = process.hrtime(startTime); 161 | this._parseDuration += dur[0] * 1e9 + dur[1]; 162 | if(msg.valid === 'VALID') { 163 | let output : Array = new Array(this._fieldCount); 164 | let values : SuppValues = msg.supportedValues; 165 | output[0] = lineNo; 166 | startTime = process.hrtime(); 167 | let field: string; 168 | for(field in values) { 169 | let fieldIdx : ?number = this._fields[field]; 170 | if(typeof fieldIdx !== 'number') { 171 | fieldIdx = this._fieldCount++; 172 | this._fields[field] = fieldIdx; 173 | this._fieldNames.push(field); 174 | output.push(msg[field]); 175 | } else { 176 | output[fieldIdx] = msg[field]; 177 | } 178 | }; 179 | let dur = process.hrtime(startTime); 180 | this._readDuration += dur[0] * 1e9 + dur[1]; 181 | return this.arrayToCsv(output); 182 | } else { 183 | if(msg.valid !== 'INCOMPLETE') { 184 | console.log('valid:' + msg.valid + ' msg:' + (msg.errMsg || '-')); 185 | this._failCount ++; 186 | this._failStream.write(line + '\n'); 187 | if(msg.valid === 'UNSUPPORTED') { 188 | let tag : string; 189 | if(isNaN(msg.aisType)) { 190 | tag = 'Non AIS'; 191 | } else { 192 | tag = String(msg.aisType); 193 | } 194 | 195 | let failed = this._failedMsgs[tag]; 196 | if(failed) { 197 | this._failedMsgs[tag] = ++failed; 198 | } else { 199 | this._failedMsgs[tag] = 1; 200 | } 201 | } 202 | } 203 | } 204 | } catch(error) { 205 | console.error('exception from AisDecode in line:' + lineNo + ' error:' + error); 206 | console.error('line content:' + rawLine); 207 | this._failCount ++; 208 | this._failStream.write(line + '\n'); 209 | } 210 | } 211 | return; 212 | } 213 | 214 | getHeaders() : string { 215 | let output : string = this.arrayToCsv(this._fieldNames); 216 | let units : Array = []; 217 | this._fieldNames.forEach((field)=>{ 218 | units.push(AisMessage.getUnit(field) || ''); 219 | }); 220 | output += this.arrayToCsv(units); 221 | return output; 222 | } 223 | 224 | getResults() : string { 225 | let failed : string = 'Unsupported Msg Types:' 226 | let key : string 227 | for(key in this._failedMsgs) { 228 | failed += ' ' + key + ':' + this._failedMsgs[key] + ','; 229 | } 230 | return 'success, read ' + this._lineCount + ' lines, ' + this._aisCount + ' ais lines of which ' + this._failCount + ' failed\n' + 231 | 'Durations: ' + (this._parseDuration / this._aisCount).toFixed(3) + ' ns per message for parsing, ' + 232 | (this._readDuration / (this._aisCount - this._failCount)).toFixed(3) + ' ns per message for reading\n' + failed; 233 | } 234 | } 235 | 236 | 237 | if(process.argv.length < 5) { 238 | console.log('USAGE: [sigk]'); 239 | } else { 240 | if(fs.existsSync(process.argv[2])) { 241 | let iStream = fs.createReadStream(process.argv[2]); 242 | let oStream = fs.createWriteStream(TMP_FILE_NAME); 243 | let fStream = fs.createWriteStream(process.argv[4]); 244 | let parser : Parser = new Parser(fStream,process.argv[5] === 'sigk'); 245 | oStream.on('finish',()=>{ 246 | iStream.close(); 247 | fStream.close(); 248 | oStream.close(); 249 | iStream = fs.createReadStream(TMP_FILE_NAME); 250 | oStream = fs.createWriteStream(process.argv[3]); 251 | oStream.on('finish',()=>{ 252 | iStream.close(); 253 | fs.unlink(TMP_FILE_NAME); 254 | oStream.close(); 255 | console.log(parser.getResults()); 256 | }); 257 | oStream.write(parser.getHeaders()); 258 | iStream.pipe(oStream); 259 | }) 260 | parser.pipe(oStream); 261 | iStream.pipe(parser); 262 | } 263 | } 264 | --------------------------------------------------------------------------------