├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── Dockerfile ├── Procfile ├── api.js ├── app.json ├── area.js ├── index.html ├── index.js ├── lib └── puppeteer.js ├── package-lock.json ├── package.json ├── readme.md └── server.js /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: publish npm package 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 10 15 | - run: npm install 16 | - uses: JS-DevTools/npm-publish@v1 17 | with: 18 | token: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM thompsnm/nodejs-chrome 2 | COPY . development/ 3 | WORKDIR development 4 | RUN npm install 5 | ENTRYPOINT [ "node", "index.js" ] 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node --inspect index.js 2 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio'); 2 | const moment = require('moment'); 3 | const request = require('request'); 4 | var brotli = require('brotli'); 5 | 6 | const debug = (...args) => { 7 | if (true) { 8 | console.log.apply(console, args); 9 | } 10 | } 11 | 12 | function parsePosition(position) { 13 | debug('Position: ', position); 14 | 15 | return { 16 | "error": position.error, 17 | "data": 18 | { 19 | timestamp: position.data.timestamp, 20 | unixtime: position.data.unixtime, 21 | latitude: parseFloat(position.data.latitude), 22 | longitude: parseFloat(position.data.longitude), 23 | course: parseFloat(position.data.course), 24 | speed: parseFloat(position.data.speed) 25 | } 26 | } 27 | } 28 | 29 | const headersVF = { 30 | 'User-Agent': 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3703.0 Safari/537.36', 31 | 'Content-Type' : 'application/x-www-form-urlencoded', 32 | 'cache-control': 'max-age=0', 33 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', 34 | 'upgrade-insecure-requests':1, 35 | 'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8' 36 | }; 37 | 38 | const headersMT = { 39 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3703.0 Safari/537.36', 40 | 'Content-Type' : 'application/x-www-form-urlencoded', 41 | 'cache-control': 'max-age=0', 42 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', 43 | 'upgrade-insecure-requests':1, 44 | 'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8' 45 | }; 46 | 47 | 48 | function getLocationFromVF(mmsi, cb) { 49 | const url = `https://www.vesselfinder.com/vessels/somestring-MMSI-${mmsi}`; 50 | debug('getLocationFromVF', url); 51 | debug('error VF'); 52 | cb({ error: 'an unknown error occured' }); 53 | } 54 | 55 | function getLocationFromMT(mmsi, cb) { 56 | const url = `https://www.marinetraffic.com/en/data/?asset_type=vessels&columns=flag,shipname,photo,recognized_next_port,reported_eta,reported_destination,current_port,imo,mmsi,ship_type,show_on_live_map,time_of_latest_position,lat_of_latest_position,lon_of_latest_position&mmsi|eq|mmsi=${mmsi}`; 57 | debug('getLocationFromMT', url); 58 | 59 | headers = headersMT; 60 | 61 | const options = { 62 | url, 63 | headers, 64 | }; 65 | 66 | request(options, function (error, response, html) { 67 | 68 | 69 | 70 | if (!error && response.statusCode == 200 || response.statusCode == 403) { 71 | 72 | 73 | console.log('first request successsfull, set cookie'); 74 | let secondRequestHeaders = headers; 75 | secondRequestHeaders.cookie = response.headers['set-cookie']; 76 | secondRequestHeaders.referer = `https://www.marinetraffic.com/en/data/?asset_type=vessels&columns=flag,shipname,photo,recognized_next_port,reported_eta,reported_destination,current_port,imo,mmsi,ship_type,show_on_live_map,time_of_latest_position,lat_of_latest_position,lon_of_latest_position&mmsi|eq|mmsi=${mmsi}`; 77 | secondRequestHeaders['Vessel-Image'] = '007fb60815c6558c472a846479502b668e08'; 78 | 79 | request({ 80 | url: `https://www.marinetraffic.com/en/reports?asset_type=vessels&columns=flag,shipname,photo,recognized_next_port,reported_eta,reported_destination,current_port,imo,mmsi,ship_type,show_on_live_map,time_of_latest_position,lat_of_latest_position,lon_of_latest_position&mmsi=${mmsi}`, 81 | headers: secondRequestHeaders 82 | }, function (error, response, html) { 83 | 84 | if (!error && response.statusCode == 200 || response.statusCode == 403) { 85 | 86 | console.log('second request worked'); 87 | 88 | console.log(html); 89 | 90 | let parsed = JSON.parse(html); 91 | 92 | console.log(parsed); 93 | if (parsed.totalCount > 0) 94 | { 95 | 96 | const latitude = parseFloat(parsed.data[0].LAT); 97 | const longitude = parseFloat(parsed.data[0].LON); 98 | const speed = parseFloat(parsed.data[0].SPEED); 99 | const course = parseFloat(parsed.data[0].COURSE); 100 | 101 | const timestamp = new Date(parsed.data[0].LAST_POS*1000).toISOString(); 102 | const unixtime = new Date(parsed.data[0].LAST_POS*1000).getTime()/1000; 103 | console.log(123); 104 | 105 | //const $ = cheerio.load(html); 106 | console.log(timestamp, speed ,course ,latitude ,longitude) 107 | 108 | if (timestamp && latitude && longitude) { 109 | cb( 110 | parsePosition({ 111 | error: null, 112 | data: { 113 | timestamp: timestamp, 114 | unixtime, 115 | course: course, 116 | speed, 117 | latitude, 118 | longitude, 119 | } 120 | }) 121 | ); 122 | } else { 123 | cb({ error: 'missing needed position data' }); 124 | } 125 | } else { 126 | cb({ error: 'no records were found' }); 127 | } 128 | } else { 129 | cb({ error }); 130 | } 131 | 132 | }); 133 | 134 | } else { 135 | cb({ error }); 136 | } 137 | 138 | 139 | 140 | }); 141 | } 142 | 143 | function getLocation(mmsi, cb) { 144 | debug('getting location for vessel: ', mmsi); 145 | getLocationFromVF(mmsi, function(VFResult) { 146 | debug('got location from vf', VFResult); 147 | 148 | getLocationFromMT(mmsi, function (MTResult) { 149 | if (MTResult.error) { 150 | cb(VFResult); 151 | } else { 152 | debug('got location from mt', MTResult); 153 | if (!VFResult.data) { 154 | return cb(MTResult); 155 | } 156 | const vfDate = moment( VFResult.data.timestamp); 157 | const mtDate = moment(MTResult.data.timestamp); 158 | const secondsDiff = mtDate.diff(vfDate, 'seconds') 159 | debug('time diff in seconds: ', secondsDiff); 160 | 161 | cb(secondsDiff > 0 ? MTResult : VFResult); 162 | } 163 | }); 164 | }); 165 | } 166 | 167 | function getVesselsInPort(shipPort, cb) { 168 | const url = `https://www.marinetraffic.com/en/reports?asset_type=vessels&columns=flag,shipname,photo,recognized_next_port,reported_eta,reported_destination,current_port,imo,ship_type,show_on_live_map,time_of_latest_position,lat_of_latest_position,lon_of_latest_position,current_port_country,notes¤t_port_in_name=${shipPort}`; 169 | debug('getVesselsInPort', url); 170 | 171 | const headers={ 172 | 'accept': '*/*', 173 | 'Accept-Language': 'en-US,en;q=0.5', 174 | 'Accept-Encoding': 'gzip, deflate, brotli', 175 | 'Vessel-Image': '0053e92efe9e7772299d24de2d0985adea14', 176 | 'X-Requested-With': 'XMLHttpRequest' 177 | } 178 | const options = { 179 | url, 180 | headers, 181 | json: true, 182 | gzip: true, 183 | deflate: true, 184 | brotli:true 185 | }; 186 | request(options, function (error, response, html) { 187 | if (!error && response.statusCode == 200 || typeof response != 'undefined' && response.statusCode == 403) { 188 | 189 | 190 | return cb(response.body.data.map((vessel) => ({ 191 | name: vessel.SHIPNAME, 192 | id: vessel.SHIP_ID, 193 | lat: Number(vessel.LAT), 194 | lon: Number(vessel.LON), 195 | timestamp: vessel.LAST_POS, 196 | mmsi: vessel.MMSI, 197 | imo: vessel.IMO, 198 | callsign: vessel.CALLSIGN, 199 | speed: Number(vessel.SPEED), 200 | area: vessel.AREA_CODE, 201 | type: vessel.TYPE_SUMMARY, 202 | country: vessel.COUNTRY, 203 | destination: vessel.DESTINATION, 204 | port_current_id: vessel.PORT_ID, 205 | port_current: vessel.CURRENT_PORT, 206 | port_next_id: vessel.NEXT_PORT_ID, 207 | port_next: vessel.NEXT_PORT_NAME, 208 | }))); 209 | } else { 210 | debug('error in getVesselsInPort'); 211 | cb({ error: 'an unknown error occured' }); 212 | return false; 213 | } 214 | }); 215 | } 216 | 217 | 218 | module.exports = { 219 | getLocationFromVF: getLocationFromVF, 220 | getLocationFromMT: getLocationFromMT, 221 | getLocation: getLocation, 222 | getVesselsInPort:getVesselsInPort, 223 | }; 224 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AIS API", 3 | "description": "Node AIS API Heroku app", 4 | "logo": "https://cdn.jsdelivr.net/gh/heroku/node-js-getting-started/public/node.svg", 5 | "keywords": ["api", "ais"], 6 | "image": "heroku/nodejs" 7 | } 8 | -------------------------------------------------------------------------------- /area.js: -------------------------------------------------------------------------------- 1 | const scraper = require('./lib/puppeteer'); 2 | 3 | const fetchResultByArea = async (area, time, cb) => { 4 | const result = await scraper.fetch({ 5 | url: `https://www.marinetraffic.com/en/reports?asset_type=vessels&columns=time_of_latest_position:desc,flag,shipname,photo,recognized_next_port,reported_eta,reported_destination,current_port,imo,ship_type,show_on_live_map,area,lat_of_latest_position,lon_of_latest_position&area_in=${area}&time_of_latest_position_between=${time}`, 6 | referer: 'https://www.marinetraffic.com/en/data/?asset_type=vessels&columns=time_of_latest_position:desc,flag,shipname,photo,recognized_next_port,reported_eta,reported_destination,current_port,imo,ship_type,show_on_live_map,area,lat_of_latest_position,lon_of_latest_position&area_in|in|West%20Mediterranean,East%20Mediterranean|area_in=WMED,EMED&time_of_latest_position_between|gte|time_of_latest_position_between=60,525600', 7 | responseSelector: '/en/reports?', 8 | extraHeaders: { 9 | 'vessel-image': '0053e92efe9e7772299d24de2d0985adea14', 10 | }, 11 | }, cb); 12 | } 13 | 14 | const mapResult = (result) => { 15 | return result.data.map((vessel) => ({ 16 | name: vessel.SHIPNAME, 17 | id: vessel.SHIP_ID, 18 | lat: Number(vessel.LAT), 19 | lon: Number(vessel.LON), 20 | timestamp: vessel.LAST_POS, 21 | mmsi: vessel.MMSI, 22 | imo: vessel.IMO, 23 | callsign: vessel.CALLSIGN, 24 | speed: Number(vessel.SPEED), 25 | area: vessel.AREA_CODE, 26 | type: vessel.TYPE_SUMMARY, 27 | country: vessel.COUNTRY, 28 | destination: vessel.DESTINATION, 29 | port_current_id: vessel.PORT_ID, 30 | port_current: vessel.CURRENT_PORT, 31 | port_next_id: vessel.NEXT_PORT_ID, 32 | port_next: vessel.NEXT_PORT_NAME, 33 | })); 34 | } 35 | 36 | const fetchVesselsInArea = (regions = ['WMED','EMED'], cb) => { 37 | const timeframe = [60, 525600]; 38 | fetchResultByArea(regions.join(','), timeframe.join(','), (result) => { 39 | if (!result || !result.data.length) { 40 | return cb(null); 41 | } 42 | return cb(mapResult(result)); 43 | }); 44 | } 45 | 46 | const fetchResultNearMe = async (lat, lng, distance, time, cb) => { 47 | const result = await scraper.fetch({ 48 | url: `https://www.marinetraffic.com/en/reports?asset_type=vessels&columns=time_of_latest_position:desc,flag,shipname,photo,recognized_next_port,reported_eta,reported_destination,current_port,imo,ship_type,show_on_live_map,area,lat_of_latest_position,lon_of_latest_position&time_of_latest_position_between=${time}&near_me=${lat},${lng},${distance}`, 49 | referer: 'https://www.marinetraffic.com/en/data/?asset_type=vessels&columns=time_of_latest_position:desc,flag,shipname,photo,recognized_next_port,reported_eta,reported_destination,current_port,imo,ship_type,show_on_live_map,area,lat_of_latest_position,lon_of_latest_position&area_in|in|West%20Mediterranean,East%20Mediterranean|area_in=WMED,EMED&time_of_latest_position_between|gte|time_of_latest_position_between=60,525600', 50 | responseSelector: '/en/reports?', 51 | extraHeaders: { 52 | 'vessel-image': '0053e92efe9e7772299d24de2d0985adea14', 53 | }, 54 | }, cb); 55 | } 56 | 57 | const fetchVesselsNearMe = (lat = 51.74190, lng = 3.89773, distance = 2, cb) => { 58 | const timeframe = [60, 525600]; 59 | fetchResultNearMe(lat, lng, distance, timeframe.join(','), (result) => { 60 | if (!result || !result.data.length) { 61 | return cb(null); 62 | } 63 | return cb(mapResult(result)); 64 | 65 | }); 66 | } 67 | 68 | module.exports = { 69 | fetchVesselsInArea, 70 | fetchVesselsNearMe, 71 | }; 72 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |
I looked for a free API solution to access machine readable AIS Data. This solution uses the free web solutions to crawl the data and returns them in json.
3 |This is a nodejs web app.
5 |Takes position from MT and from VT and returns the newest 8 | example: http://host:port/getLastPosition/211281610
9 |Returns position from VF 11 | example: http://host:port/getLastPositionFromVF/211281610
12 |Returns position from MT 14 | example: http://host:port/getLastPositionFromMT/211281610
15 |Returns all vessels in area, defined by a list of area keywords 17 | example: http://host:port/getVesselsInArea/WMED,EMED
18 |[{
19 | name: vessel.SHIPNAME,
20 | id: vessel.SHIP_ID,
21 | lat: Number(vessel.LAT),
22 | lon: Number(vessel.LON),
23 | timestamp: vessel.LAST_POS,
24 | mmsi: vessel.MMSI,
25 | imo: vessel.IMO,
26 | callsign: vessel.CALLSIGN,
27 | speed: Number(vessel.SPEED),
28 | area: vessel.AREA_CODE,
29 | type: vessel.TYPE_SUMMARY,
30 | country: vessel.COUNTRY,
31 | destination: vessel.DESTINATION,
32 | port_current_id: vessel.PORT_ID,
33 | port_current: vessel.CURRENT_PORT,
34 | port_next_id: vessel.NEXT_PORT_ID,
35 | port_next: vessel.NEXT_PORT_NAME,
36 | },…]
37 |
38 | Returns all vessels in a port, named after the MT nomenclature
40 | example: http://host:port/getVesselsInPort/piraeus
41 | Output format identical to getVesselsInArea
Requirements: npm & nodejs.
44 |clone this repo
47 |run npm install
run node index.js
This application can be easily deployed to heroku, simply install the heroku cli and run the following commands:
57 |heroku create
git push heroku master