143 lines
4.5 KiB
JavaScript
143 lines
4.5 KiB
JavaScript
import {cfg} from './config.mjs';
|
|
import * as fs from 'node:fs';
|
|
import { resolve, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const CHANNEL_MAP = {0: 'A, B', 1: 'A', 2: 'B'};
|
|
const SENDER_TYPE = {
|
|
0: 'Other', 1: 'Unknown', 2: 'Cargo', 3: 'Class B',
|
|
4: 'Passenger', 5: 'Special', 6: 'Tanker', 7: 'High Speed',
|
|
8: 'Fishing', 9: 'Aircraft', 10: 'Helicopter', 11: 'Base Station',
|
|
12: 'AtoN', 13: 'SART/EPIRB'
|
|
};
|
|
|
|
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
const DATA = resolve(DIR, '../data');
|
|
const IMAGES_DIR = resolve(DATA, 'ships');
|
|
|
|
let aisCache = null;
|
|
let aisCacheTs = 0;
|
|
const AIS_TTL = 1000;
|
|
|
|
function decodeMsgTypes(raw) {
|
|
const types = [];
|
|
for (let i = 0; i < 32; i++) {
|
|
if (raw & (1 << i)) types.push(i);
|
|
}
|
|
return types;
|
|
}
|
|
|
|
function decodeFlags(flags) {
|
|
return {
|
|
validated: flags & 3,
|
|
validated2: (flags & 3) == 2 ? -1 : flags & 3,
|
|
repeat: (flags >> 2) & 3,
|
|
virtual_aid: (flags >> 4) & 1,
|
|
approx: (flags >> 5) & 1,
|
|
channels: (flags >> 6) & 15,
|
|
cs_unit: (flags >> 10) & 3,
|
|
raim: (flags >> 12) & 3,
|
|
dte: (flags >> 14) & 3,
|
|
assigned: (flags >> 16) & 3,
|
|
display: (flags >> 18) & 3,
|
|
dsc: (flags >> 20) & 3,
|
|
band: (flags >> 22) & 3,
|
|
msg22: (flags >> 24) & 3,
|
|
off_position: (flags >> 26) & 3,
|
|
maneuver: (flags >> 28) & 3,
|
|
};
|
|
}
|
|
|
|
export async function getAIS() {
|
|
if (aisCache && Date.now() - aisCacheTs < AIS_TTL) return aisCache;
|
|
const {ADSB_URL} = cfg();
|
|
if (!ADSB_URL) return {data: []};
|
|
const r = await fetch(`${ADSB_URL}:9990/api/ships_array.json`);
|
|
const {dynamic, static: staticRows, time} = await r.json();
|
|
|
|
const staticMap = new Map(staticRows.map(row => [row[0], row]));
|
|
|
|
aisCache = dynamic.map(row => {
|
|
const [mmsi, lat, lon, distance, bearing, heading,
|
|
cog, speed, status, rrsi, ppm, count, msg_type,
|
|
last_signal, last_group, group_mask, flags, altitude,
|
|
received_stations, mmsi_type, shipclass, country] = row;
|
|
const s = staticMap.get(mmsi) ?? [];
|
|
const [_mmsi, shipname, callsign, destination, shiptype,
|
|
imo, to_bow, to_stern, to_port, to_starboard, draught,
|
|
eta_month, eta_day, eta_hour, eta_minute] = s;
|
|
|
|
const f = decodeFlags(flags);
|
|
|
|
return {
|
|
mmsi,
|
|
type: SENDER_TYPE[shipclass],
|
|
lat, lon, altitude,
|
|
heading, cog, speed,
|
|
distance, bearing,
|
|
destination,
|
|
rrsi, ppm, count,
|
|
msg_type: decodeMsgTypes(msg_type),
|
|
channels: CHANNEL_MAP[f.channels] ?? f.channels,
|
|
...f,
|
|
shiptype, imo, draught,
|
|
country, callsign,
|
|
shipname,
|
|
to_bow, to_stern, to_port, to_starboard,
|
|
eta_month, eta_day, eta_hour, eta_minute,
|
|
last_signal: last_signal ? time - last_signal : null,
|
|
last_group, group_mask,
|
|
received_stations, shipclass,
|
|
status
|
|
};
|
|
});
|
|
aisCacheTs = Date.now();
|
|
return aisCache;
|
|
}
|
|
|
|
export async function getAISImage(mmsi) {
|
|
if(!mmsi) return null;
|
|
mmsi = String(mmsi);
|
|
|
|
const filePath = resolve(IMAGES_DIR, `${mmsi}.jpg`);
|
|
if (fs.existsSync(filePath)) return fs.readFileSync(filePath);
|
|
|
|
function randomUA() {
|
|
const v = 36 + Math.floor(Math.random() * 40);
|
|
const builds = [
|
|
`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`,
|
|
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`,
|
|
`Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`,
|
|
];
|
|
return builds[~~(Math.random() * builds.length)];
|
|
}
|
|
|
|
const html = await fetch(`https://www.vesselfinder.com/vessels/details/${mmsi}`, {
|
|
headers: {'Accept': '*/*', 'User-Agent': randomUA(), 'Referer': 'https://www.vesselfinder.com/'}
|
|
}).then(resp => {
|
|
if(resp.ok) return resp.text();
|
|
throw new Error('Failed to fetch vessel page');
|
|
});
|
|
|
|
const match = html.match(/<img.+main-photo.+>/);
|
|
if(!match) return null;
|
|
|
|
const srcMatch = match[0].match(/src="(.+?)"/);
|
|
if(!srcMatch) return null;
|
|
|
|
let imgUrl = srcMatch[1];
|
|
if(imgUrl.startsWith('//')) imgUrl = `https:${imgUrl}`;
|
|
if(imgUrl.startsWith('/')) imgUrl = `https://www.vesselfinder.com${imgUrl}`;
|
|
|
|
const resp = await fetch(imgUrl, {
|
|
headers: {'Accept': '*/*', 'User-Agent': randomUA(), 'Referer': 'https://www.vesselfinder.com/'}
|
|
});
|
|
|
|
if(!resp.ok) throw new Error('Failed to fetch image');
|
|
|
|
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
fs.mkdirSync(IMAGES_DIR, { recursive: true });
|
|
fs.writeFileSync(filePath, buffer);
|
|
return buffer;
|
|
}
|