.
675 |
--------------------------------------------------------------------------------
/dist/static/index.html:
--------------------------------------------------------------------------------
1 | locationsource
2 | Solution to access machine readable AIS Data. This solution uses the free web solutions to crawl the data and returns them in json.
3 | As this repo is a successor to the ais-api, lecacy parts are added (see legacy) Those will be removed in future versions.
4 | Install on local machine
5 | Requirements: npm & nodejs.
6 |
7 | clone this repo
8 |
9 | run npm install
10 |
11 | run npm run dev
12 |
13 |
14 |
15 | All locations are returned in this format:
16 | {
17 | timestamp: ISO 8601
18 | latitude:
19 | longitude:
20 | course: in deg
21 | speed: in kn
22 | source:
23 | source_type: e.g. ais
24 | raw_data: (contains all the raw data)
25 | }
26 | Legacy Paths
27 | As this repo is a successor to the ais-api, some legacy paths are added. Those will be removed in future versions
28 | /legacy/getLastPosition/:mmsi
29 | Takes position from MT and from VT and returns the newest
30 | example: http://localhost:5000/legacy/getLastPosition/211879870
31 | /legacy/getLastPositionFromVF/:mmsi
32 | Returns position from VF
33 | example: http://localhost:5000/legacy/getLastPositionFromVF/211281610
34 | /legacy/getLastPositionFromMT/:mmsi
35 | Returns position from MT
36 | example: http://localhost:5000/legacy/getLastPositionFromMT/211281610
37 | /legacy/getVesselsInArea/:area
38 | Returns all vessels in area, defined by a list of area keywords
39 | example: http://localhost:5000/legacy/getVesselsInArea/WMED,EMED
40 | /legacy/getVesselsNearMe/:lat/:lng/:distance
41 | Returns all vessels near me, defined by a location in latitude, longitude, and distance
42 | example: http://localhost:5000/legacy/getVesselsNearMe/51.74190/3.89773/2
43 | [{
44 | name: vessel.SHIPNAME,
45 | id: vessel.SHIP_ID,
46 | lat: Number(vessel.LAT),
47 | lon: Number(vessel.LON),
48 | timestamp: vessel.LAST_POS,
49 | mmsi: vessel.MMSI,
50 | imo: vessel.IMO,
51 | callsign: vessel.CALLSIGN,
52 | speed: Number(vessel.SPEED),
53 | area: vessel.AREA_CODE,
54 | type: vessel.TYPE_SUMMARY,
55 | country: vessel.COUNTRY,
56 | destination: vessel.DESTINATION,
57 | port_current_id: vessel.PORT_ID,
58 | port_current: vessel.CURRENT_PORT,
59 | port_next_id: vessel.NEXT_PORT_ID,
60 | port_next: vessel.NEXT_PORT_NAME,
61 | },…]
62 |
63 | /legacy/getVesselsInPort/:shipPort
64 | Returns all vessels in a port, named after the MT nomenclature
65 | example: http://localhost:5000/legacy/getVesselsInPort/piraeus
66 | Output format identical to getVesselsInArea
67 | Paths
68 | /:sourcetype/:source/:vehicleidentifier/location/latest
69 | find latest position for vehicle for specified sourcetype and source
70 | example: http://localhost:5000/ais/mt/211281610/location/latest
71 | /:source/:placeidentifier/vehicles/
72 | /:source/area/vehicles
73 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "position-api" {
2 | // Define the exported types and interfaces here
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "position-api",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.ts",
6 | "types": "index.d.ts",
7 | "bin": {
8 | "position-api": "src/index.ts"
9 | },
10 | "scripts": {
11 | "build": "npx tsc",
12 | "start": "ts-node src/index.ts",
13 | "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"",
14 | "lint": "eslint --ext .ts .",
15 | "lint:fix": "eslint --ext .ts . --fix",
16 | "prettier": "prettier --ignore-path .gitignore --write \"**/*.+(ts|json)\"",
17 | "test": "echo \"Error: no test specified\" && exit 1",
18 | "postinstall": "copyfiles -n .env.template .env"
19 | },
20 | "author": "",
21 | "license": "ISC",
22 | "devDependencies": {
23 | "@types/node": "^18.19.9",
24 | "@types/node-fetch": "^2.6.4",
25 | "@typescript-eslint/eslint-plugin": "^6.13.2",
26 | "concurrently": "^7.6.0",
27 | "copyfiles": "^2.4.1",
28 | "eslint": "^8.55.0",
29 | "eslint-config-standard-with-typescript": "^40.0.0",
30 | "eslint-plugin-import": "^2.29.0",
31 | "eslint-plugin-n": "^16.3.1",
32 | "eslint-plugin-promise": "^6.1.1",
33 | "moment": "^2.30.1",
34 | "nodemon": "^2.0.20",
35 | "prettier": "^3.1.0",
36 | "typescript": "^5.3.3"
37 | },
38 | "dependencies": {
39 | "@types/cors": "^2.8.17",
40 | "@types/express": "^4.17.17",
41 | "@types/node": "^18.19.9",
42 | "brotli": "^1.3.3",
43 | "cheerio": "^1.0.0-rc.12",
44 | "cors": "^2.8.5",
45 | "dotenv": "^16.4.1",
46 | "express": "^4.19.2",
47 | "moment": "^2.30.1",
48 | "path": "^0.12.7",
49 | "puppeteer": "^19.11.1",
50 | "request": "^2.88.2"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | this repo is discontinued for now
2 |
3 | # locationsource
4 |
5 | Solution to access machine readable AIS Data. This solution uses the free web solutions to crawl the data and returns them in json.
6 |
7 | As this repo is a successor to the ais-api, lecacy parts are added (see legacy) Those will be removed in future versions.
8 |
9 |
10 | ## Install on local machine
11 |
12 | Requirements: npm & nodejs.
13 |
14 | 1. clone this repo
15 |
16 | 2. run `npm install`
17 |
18 | 3. run `npm run dev`
19 |
20 | ### Location Format
21 | All locations are returned in this format:
22 | {
23 | timestamp: ISO 8601
24 | latitude:
25 | longitude:
26 | course: in deg
27 | speed: in kn
28 | source:
29 | source_type: e.g. ais
30 | raw_data: (contains all the raw data)
31 | }
32 |
33 | ### Legacy Paths
34 |
35 | As this repo is a successor to the ais-api, some legacy paths are added. Those will be removed in future versions
36 |
37 |
38 | #### /legacy/getLastPosition/:mmsi
39 |
40 | Takes position from MT and from VT and returns the newest
41 | example: http://localhost:5000/legacy/getLastPosition/211879870
42 |
43 | #### /legacy/getLastPositionFromVF/:mmsi
44 |
45 | Returns position from VF
46 | example: http://localhost:5000/legacy/getLastPositionFromVF/211281610
47 |
48 | #### /legacy/getLastPositionFromMT/:mmsi
49 |
50 | Returns position from MT
51 | example: http://localhost:5000/legacy/getLastPositionFromMT/211281610
52 |
53 | #### /legacy/getVesselsInArea/:area
54 |
55 | Returns all vessels in area, defined by a list of area keywords
56 | example: http://localhost:5000/legacy/getVesselsInArea/WMED,EMED
57 |
58 | #### /legacy/getVesselsNearMe/:lat/:lng/:distance
59 |
60 | Returns all vessels near me, defined by a location in latitude, longitude, and distance
61 | example: http://localhost:5000/legacy/getVesselsNearMe/51.74190/3.89773/2
62 |
63 | ```Javascript
64 | [{
65 | name: vessel.SHIPNAME,
66 | id: vessel.SHIP_ID,
67 | lat: Number(vessel.LAT),
68 | lon: Number(vessel.LON),
69 | timestamp: vessel.LAST_POS,
70 | mmsi: vessel.MMSI,
71 | imo: vessel.IMO,
72 | callsign: vessel.CALLSIGN,
73 | speed: Number(vessel.SPEED),
74 | area: vessel.AREA_CODE,
75 | type: vessel.TYPE_SUMMARY,
76 | country: vessel.COUNTRY,
77 | destination: vessel.DESTINATION,
78 | port_current_id: vessel.PORT_ID,
79 | port_current: vessel.CURRENT_PORT,
80 | port_next_id: vessel.NEXT_PORT_ID,
81 | port_next: vessel.NEXT_PORT_NAME,
82 | },…]
83 | ```
84 |
85 | #### /legacy/getVesselsInPort/:shipPort
86 |
87 | Returns all vessels in a port, named after the MT nomenclature
88 | example: http://localhost:5000/legacy/getVesselsInPort/piraeus
89 |
90 | Output format identical to **getVesselsInArea**
91 |
92 |
93 | ### Paths
94 |
95 | #### /:sourcetype/:source/:vehicleidentifier/location/latest
96 | find latest position for vehicle for specified sourcetype and source
97 | example: http://localhost:5000/ais/mt/211281610/location/latest
98 |
99 | #### /:source/:placeidentifier/vehicles/
100 |
101 | #### /:source/area/vehicles
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/src/classes/server.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import cors from "cors";
3 | import path from "path";
4 | import { api } from "../legacy/api";
5 | import { areaApi } from "../legacy/area";
6 | import ADSBexchange from "./sources/adsb/adsbe";
7 | class Server {
8 | app: any;
9 | constructor(port) {
10 | this.init(port);
11 | }
12 |
13 | init(port: number) {
14 | this.app = express();
15 | this.app.set("port", port);
16 | this.app.use(
17 | cors({
18 | origin: "*",
19 | }),
20 | );
21 | this.app.get("/", (_request: any, response: any) => {
22 | response.sendFile(path.join(__dirname, "/../static/index.html"));
23 | });
24 | this.loadLegacyRoutes();
25 | this.loadRoutes();
26 | this.app.listen(this.app.get("port"), () => {
27 | console.log("Node this.app is running on port", this.app.get("port"));
28 | });
29 | }
30 |
31 | loadRoutes() {
32 | // /:sourcetype/:source/:vehicleidentifier/location/latest
33 | this.app.get(
34 | "/adsb/adsbe/:icao/location/latest",
35 | async (req: any, res: any) => {
36 | console.log(req.params.icao);
37 | const adsbe = new ADSBexchange();
38 | const location = await adsbe.getLocation(req.params.icao);
39 | console.log(location);
40 | res.send({
41 | error: null,
42 | data: location,
43 | });
44 | },
45 | );
46 | }
47 |
48 | loadLegacyRoutes() {
49 | // this route is wrongly named on purpose for legacy reasons.
50 | // AS VF is not as easy to reverse as the other ones, it is replaced by MST
51 | this.app.get(
52 | "/legacy/getLastPositionFromVF/:mmsi",
53 | (req: any, res: any) => {
54 | api.getLocationFromMST(req.params.mmsi, (result) => {
55 | res.send(result);
56 | });
57 | },
58 | );
59 | this.app.get(
60 | "/legacy/getLastPositionFromMT/:mmsi",
61 | (req: any, res: any) => {
62 | api.getLocationFromMT(req.params.mmsi, (result) => {
63 | res.send(result);
64 | });
65 | },
66 | );
67 | this.app.get("/legacy/getLastPosition/:mmsi", (req: any, res: any) => {
68 | api.getLocation(req.params.mmsi, (result) => {
69 | res.send(result);
70 | });
71 | });
72 | // e.g. /getVesselsInArea/WMED,EMED
73 | this.app.get(
74 | "/legacy/getVesselsInArea/:area",
75 | async (req: any, res: any) => {
76 | await areaApi.fetchVesselsInArea(
77 | req.params.area.split(","),
78 | (result) => {
79 | res.json(result);
80 | },
81 | );
82 | },
83 | );
84 | this.app.get(
85 | "/legacy/getVesselsNearMe/:lat/:lng/:distance",
86 | async (req: any, res: any) => {
87 | await areaApi.fetchVesselsNearMe(
88 | req.params.lat,
89 | req.params.lng,
90 | req.params.distance,
91 | (result) => {
92 | res.json(result);
93 | },
94 | );
95 | },
96 | );
97 | this.app.get("/legacy/getVesselsInPort/:shipPort", (req: any, res: any) => {
98 | api.getVesselsInPort(req.params.shipPort, (result) => {
99 | res.send(result);
100 | });
101 | });
102 | }
103 | }
104 |
105 | export default Server;
106 |
--------------------------------------------------------------------------------
/src/classes/sources/Position.d.ts:
--------------------------------------------------------------------------------
1 | declare module "PositionTypes" {
2 | export interface Position {
3 | timestamp: string;
4 | latitude: number;
5 | longitude: number;
6 | course: number;
7 | speed: number;
8 | source: string;
9 | source_type: string;
10 | altitude?: number;
11 | raw?: any;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/classes/sources/Source.ts:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 | import puppeteer from "puppeteer";
3 |
4 | class Source {
5 | browser: any;
6 | constructor() {
7 | this.browser = false;
8 | }
9 |
10 | getBrowser: any = async () => {
11 | if (this.browser) {
12 | return this.browser;
13 | }
14 | this.browser = await puppeteer.launch({
15 | headless: true,
16 | defaultViewport: {
17 | width: 1280, // Width of a MacBook screen
18 | height: 1400, // Height of a MacBook screen
19 | },
20 | // waitUntil: "domcontentloaded",
21 | args: [
22 | "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
23 | ],
24 | });
25 | return this.browser;
26 | };
27 |
28 | convertRawCoordinatesIntoDecimal(input): number {
29 | const grade = parseInt(input.substring(0, input.indexOf("°")));
30 | const rest = input.substring(input.indexOf("°") + 1);
31 | const minutes = parseInt(rest.substring(0, rest.indexOf("'")));
32 | const seconds = parseInt(
33 | rest.substring(rest.indexOf("'") + 1).split('"')[0],
34 | );
35 | return grade + (minutes + seconds / 60) / 60;
36 | }
37 |
38 | fetch = async function (url: string, headers: any, method: string) {
39 | const response = await fetch(url, {
40 | headers,
41 | body: undefined,
42 | method,
43 | });
44 | const text = await response.text();
45 | return text;
46 | };
47 | }
48 |
49 | export default Source;
50 |
--------------------------------------------------------------------------------
/src/classes/sources/adsb/adsbe.ts:
--------------------------------------------------------------------------------
1 | import Source from "../Source";
2 | import fetch from "node-fetch";
3 |
4 | class ADSBExchange extends Source {
5 | parseLocation = async function (result: any) {
6 | const location = {
7 | timestamp: result.timestamp,
8 | latitude: result.latitude,
9 | longitude: result.longitude,
10 | course: result.course,
11 | speed: result.speed,
12 | raw: result.raw,
13 | source: "adsbExchange",
14 | source_type: "adsb",
15 | };
16 | return location;
17 | };
18 |
19 | getLocation = async (icao: string) => {
20 | ///
21 | const response = await fetch(
22 | `https://globe.adsbexchange.com/data/traces/${icao.slice(
23 | -2,
24 | )}/trace_recent_${icao}.json`,
25 | {
26 | headers: {
27 | accept: "application/json, text/javascript, */*; q=0.01",
28 | "accept-language": "de-DE,de;q=0.9",
29 | "sec-ch-ua":
30 | '"Chromium";v="116", "Not)A;Brand";v="24", "Brave";v="116"',
31 | "sec-ch-ua-mobile": "?0",
32 | "sec-ch-ua-platform": '"macOS"',
33 | "sec-fetch-dest": "empty",
34 | "sec-fetch-mode": "cors",
35 | "sec-fetch-site": "same-origin",
36 | "sec-gpc": "1",
37 | "x-requested-with": "XMLHttpRequest",
38 | Referer: "https://globe.adsbexchange.com/?icao=39e68b",
39 | "Referrer-Policy": "strict-origin-when-cross-origin",
40 | },
41 | body: undefined,
42 | method: "GET",
43 | },
44 | );
45 | const bodyjson = await response.json();
46 | const timestamp = bodyjson.timestamp;
47 | const trace = bodyjson.trace;
48 | const last_trace = trace[trace.length - 1];
49 | const [time_offset, lat, lon, alt_baro, ground_speed, ground_track] =
50 | last_trace;
51 | const last_trace_timestamp = timestamp + time_offset;
52 | const location = this.parseLocation({
53 | timestamp: last_trace_timestamp,
54 | latitude: lat,
55 | longitude: lon,
56 | course: ground_track,
57 | speed: ground_speed, // knots
58 | altitude: alt_baro,
59 | raw: bodyjson,
60 | });
61 | return await location;
62 | };
63 |
64 | getLocationFull = async (icao: string) => {
65 | ///
66 | const response = await fetch(
67 | `https://globe.adsbexchange.com/data/traces/${icao.slice(
68 | -2,
69 | )}/trace_full_${icao}.json`,
70 | {
71 | headers: {
72 | accept: "application/json, text/javascript, */*; q=0.01",
73 | "accept-language": "de-DE,de;q=0.9",
74 | "sec-ch-ua":
75 | '"Chromium";v="116", "Not)A;Brand";v="24", "Brave";v="116"',
76 | "sec-ch-ua-mobile": "?0",
77 | "sec-ch-ua-platform": '"macOS"',
78 | "sec-fetch-dest": "empty",
79 | "sec-fetch-mode": "cors",
80 | "sec-fetch-site": "same-origin",
81 | "sec-gpc": "1",
82 | "x-requested-with": "XMLHttpRequest",
83 | Referer: "https://globe.adsbexchange.com/?icao=39e68b",
84 | "Referrer-Policy": "strict-origin-when-cross-origin",
85 | },
86 | body: undefined,
87 | method: "GET",
88 | },
89 | );
90 | const bodyjson = await response.json();
91 | const timestamp = bodyjson.timestamp;
92 | const trace = bodyjson.trace;
93 |
94 | console.log(trace);
95 | const last_trace = trace[trace.length - 1];
96 | const [
97 | time_offset,
98 | lat,
99 | lon,
100 | alt_baro,
101 | ground_speed,
102 | unknown2,
103 | unknown3,
104 | unknown4,
105 | unknown5,
106 | ] = last_trace;
107 | console.log("check that lat and lon in correct order!!");
108 | console.log("last trace");
109 | console.log(last_trace);
110 | console.log("=========");
111 | const last_trace_timestamp = timestamp + time_offset;
112 | console.log(last_trace_timestamp);
113 | console.log(new Date(last_trace_timestamp * 1000));
114 | console.log(
115 | time_offset,
116 | lat,
117 | lon,
118 | alt_baro,
119 | ground_speed,
120 | unknown2,
121 | unknown3,
122 | unknown4,
123 | unknown5,
124 | );
125 | };
126 | }
127 |
128 | export default ADSBExchange;
129 |
--------------------------------------------------------------------------------
/src/classes/sources/ais/mst.ts:
--------------------------------------------------------------------------------
1 | import Source from "../Source";
2 | class MyShipTracking extends Source {
3 | parseLocation = async function (result: any) {
4 | const location = {
5 | timestamp: result.timestamp,
6 | latitude: result.latitude,
7 | longitude: result.longitude,
8 | course: result.course,
9 | speed: result.speed,
10 | source: "myshiptracking.com",
11 | source_type: "AIS",
12 | };
13 | return location;
14 | };
15 |
16 | // the method in the sources class does not work if no seconds are in the string
17 | dmsToDecimalDegreesMST = function (dms) {
18 | console.log(dms);
19 | return parseFloat(dms);
20 | };
21 |
22 | getLocation = async (mmsi: number) => {
23 | const result = await this.fetch(
24 | "https://www.myshiptracking.com/vessels/mmsi-" + mmsi,
25 | {
26 | accept:
27 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
28 | "accept-language": "de-DE,de;q=0.6",
29 | "cache-control": "max-age=0",
30 | "sec-ch-ua":
31 | '"Chromium";v="116", "Not)A;Brand";v="24", "Brave";v="116"',
32 | "sec-ch-ua-mobile": "?0",
33 | "sec-ch-ua-platform": '"macOS"',
34 | "sec-fetch-dest": "document",
35 | "sec-fetch-mode": "navigate",
36 | "sec-fetch-site": "none",
37 | "sec-fetch-user": "?1",
38 | "sec-gpc": "1",
39 | "upgrade-insecure-requests": "1",
40 | cookie:
41 | "usr_lang_exist=1; port_tz=LT; user_tz=MT; user_df=1; session_id_sp_trk=fd1i0pb2mt6i4nra9c1mejsmvk; offset=Europe%2FBerlin; usr_lang_exist=1; io=ilOBwd-Xh9Z1fGzpFaM0",
42 | },
43 | "GET",
44 | );
45 | const pattern =
46 | /(Longitude|Latitude|Course|Speed|Position Received)<\/th>\s* | (.*?)<\/td>/g;
47 | const extractedData: any = {};
48 | let match, parsedDate;
49 |
50 | while ((match = pattern.exec(result)) !== null) {
51 | const key = match[1];
52 | const value = match[2];
53 | extractedData[key] = value;
54 | }
55 |
56 | console.log(extractedData["Position Received"]);
57 | console.log(123);
58 |
59 | // Extract the date and time string using regular expression
60 | const regex = /title="([^"]+)"/;
61 | const matchd = extractedData["Position Received"].match(regex);
62 |
63 | if (matchd?.[1]) {
64 | const dateTimeString = matchd[1];
65 |
66 | // Parse the extracted date and time string
67 | const [datePart, timePart] = dateTimeString.split(" ");
68 | const [year, month, day] = datePart.split("-");
69 | const [hour, minute] = timePart.split(":");
70 |
71 | // JavaScript months are 0-based, so subtract 1 from the month
72 | parsedDate = new Date(year, month - 1, day, hour, minute);
73 |
74 | console.log(parsedDate); // This will log the parsed date object
75 | } else {
76 | console.log("Date not found in the input string.");
77 | }
78 |
79 | const position = {
80 | latitude: this.dmsToDecimalDegreesMST(extractedData.Latitude),
81 | longitude: this.dmsToDecimalDegreesMST(extractedData.Longitude),
82 | course: 10,
83 | speed: parseFloat(extractedData.Speed),
84 | timestamp: new Date(parsedDate).toISOString(),
85 | };
86 | console.log(position);
87 | return position;
88 | };
89 | }
90 |
91 | export default MyShipTracking;
92 |
--------------------------------------------------------------------------------
/src/classes/sources/ais/mt.ts:
--------------------------------------------------------------------------------
1 | import Source from "../Source";
2 | class Marinetraffic extends Source {
3 | parseLocation = async function (result: any) {
4 | const location = {
5 | timestamp: result.timestamp,
6 | latitude: result.latitude,
7 | longitude: result.longitude,
8 | course: result.course,
9 | speed: result.speed,
10 | source: "Marinetraffic",
11 | source_type: "AIS",
12 | };
13 | return location;
14 | };
15 |
16 | getLocation = async (mmsi: number) => {
17 | const browser = await this.getBrowser();
18 | const page = await browser.newPage();
19 |
20 | const url =
21 | "https://www.marinetraffic.com/en/ais/details/ships/mmsi:" + mmsi;
22 | await page.goto(url);
23 | // let parsedData = null;
24 |
25 | const waitForResponse = new Promise((resolve) => {
26 | page.on("response", async (response) => {
27 | const request = response.request();
28 | if (request.url().includes("latestPosition")) {
29 | const jsonresult = await response.text();
30 | const parsedData = JSON.parse(jsonresult);
31 | resolve(parsedData);
32 | }
33 | });
34 | });
35 |
36 | const parsedData: any = await waitForResponse;
37 | browser.close();
38 | const result = {
39 | course: parseFloat(parsedData.course),
40 | speed: parseFloat(parsedData.speed),
41 | latitude: parseFloat(parsedData.lat),
42 | longitude: parseFloat(parsedData.lon),
43 | timestamp: new Date(parsedData.lastPos * 1000).toISOString(), // assuming lastPos is in seconds
44 | };
45 | return await this.parseLocation(result);
46 | };
47 | }
48 |
49 | export default Marinetraffic;
50 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ts-node
2 | import dotenv from "dotenv";
3 | import Server from "./classes/server";
4 |
5 | dotenv.config();
6 |
7 | const port = process.env.PORT ?? 5000;
8 | // eslint-disable-next-line no-new
9 | new Server(port);
10 |
--------------------------------------------------------------------------------
/src/legacy/api.ts:
--------------------------------------------------------------------------------
1 | import Marinetraffic from "../classes/sources/ais/mt";
2 | import MyShipTracking from "../classes/sources/ais/mst";
3 |
4 | const moment = require("moment");
5 | const request = require("request");
6 |
7 | const debug = (...args) => {
8 | if (true) {
9 | console.log.apply(console, args);
10 | }
11 | };
12 |
13 | async function getLocationFromVF(mmsi, cb) {
14 | await getLocationFromMST(mmsi, cb);
15 | }
16 |
17 | async function getLocationFromMST(mmsi, cb) {
18 | const mst = new MyShipTracking();
19 | try {
20 | const location = await mst.getLocation(mmsi);
21 | console.log(location);
22 | cb({
23 | error: null,
24 | data: location,
25 | });
26 | } catch (e) {
27 | cb({
28 | error: e,
29 | data: null,
30 | });
31 | }
32 | }
33 |
34 | async function getLocationFromMT(mmsi, cb) {
35 | const mt = new Marinetraffic();
36 |
37 | try {
38 | const location = await mt.getLocation(mmsi);
39 | console.log(location);
40 | cb({
41 | error: null,
42 | data: location,
43 | });
44 | } catch (e) {
45 | cb({
46 | error: e,
47 | data: null,
48 | });
49 | }
50 | }
51 |
52 | function getLocation(mmsi, cb) {
53 | debug("getting location for vessel: ", mmsi);
54 | getLocationFromVF(mmsi, function (VFResult) {
55 | debug("got location from vf", VFResult);
56 |
57 | getLocationFromMT(mmsi, function (MTResult) {
58 | if (MTResult.error) {
59 | cb(VFResult);
60 | } else {
61 | debug("got location from mt", MTResult);
62 | if (!VFResult.data) {
63 | return cb(MTResult);
64 | }
65 | const vfDate = moment(VFResult.data.timestamp);
66 | const mtDate = moment(MTResult.data.timestamp);
67 | const secondsDiff = mtDate.diff(vfDate, "seconds");
68 | debug("time diff in seconds: ", secondsDiff);
69 |
70 | cb(secondsDiff > 0 ? MTResult : VFResult);
71 | }
72 | });
73 | });
74 | }
75 |
76 | function getVesselsInPort(shipPort, cb) {
77 | 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}`;
78 | debug("getVesselsInPort", url);
79 |
80 | const headers = {
81 | accept: "*/*",
82 | "Accept-Language": "en-US,en;q=0.5",
83 | "Accept-Encoding": "gzip, deflate, brotli",
84 | "Vessel-Image": "0053e92efe9e7772299d24de2d0985adea14",
85 | "X-Requested-With": "XMLHttpRequest",
86 | };
87 | const options = {
88 | url,
89 | headers,
90 | json: true,
91 | gzip: true,
92 | deflate: true,
93 | brotli: true,
94 | };
95 | request(options, function (error, response, html) {
96 | if (
97 | (!error && response.statusCode === 200) ||
98 | (typeof response !== "undefined" && response.statusCode === 403)
99 | ) {
100 | return cb(
101 | response.body.data.map((vessel) => ({
102 | name: vessel.SHIPNAME,
103 | id: vessel.SHIP_ID,
104 | lat: Number(vessel.LAT),
105 | lon: Number(vessel.LON),
106 | timestamp: vessel.LAST_POS,
107 | mmsi: vessel.MMSI,
108 | imo: vessel.IMO,
109 | callsign: vessel.CALLSIGN,
110 | speed: Number(vessel.SPEED),
111 | area: vessel.AREA_CODE,
112 | type: vessel.TYPE_SUMMARY,
113 | country: vessel.COUNTRY,
114 | destination: vessel.DESTINATION,
115 | port_current_id: vessel.PORT_ID,
116 | port_current: vessel.CURRENT_PORT,
117 | port_next_id: vessel.NEXT_PORT_ID,
118 | port_next: vessel.NEXT_PORT_NAME,
119 | })),
120 | );
121 | } else {
122 | debug("error in getVesselsInPort");
123 | cb({ error: "an unknown error occured" });
124 | return false;
125 | }
126 | });
127 | }
128 |
129 | export class api {
130 | static getLocationFromVF = getLocationFromVF;
131 | static getLocationFromMT = getLocationFromMT;
132 | static getLocationFromMST = getLocationFromMST;
133 | static getLocation = getLocation;
134 | static getVesselsInPort = getVesselsInPort;
135 | }
136 |
--------------------------------------------------------------------------------
/src/legacy/area.ts:
--------------------------------------------------------------------------------
1 | const scraper = require("./lib/puppeteer");
2 |
3 | const fetchResultByArea = async (area, time, cb) => {
4 | await scraper.fetch(
5 | {
6 | 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}`,
7 | referer:
8 | "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",
9 | responseSelector: "/en/reports?",
10 | extraHeaders: {
11 | "vessel-image": "0053e92efe9e7772299d24de2d0985adea14",
12 | },
13 | },
14 | cb,
15 | );
16 | };
17 |
18 | const mapResult = (result) => {
19 | return result.data.map((vessel) => ({
20 | name: vessel.SHIPNAME,
21 | id: vessel.SHIP_ID,
22 | lat: Number(vessel.LAT),
23 | lon: Number(vessel.LON),
24 | timestamp: vessel.LAST_POS,
25 | mmsi: vessel.MMSI,
26 | imo: vessel.IMO,
27 | callsign: vessel.CALLSIGN,
28 | speed: Number(vessel.SPEED),
29 | area: vessel.AREA_CODE,
30 | type: vessel.TYPE_SUMMARY,
31 | country: vessel.COUNTRY,
32 | destination: vessel.DESTINATION,
33 | port_current_id: vessel.PORT_ID,
34 | port_current: vessel.CURRENT_PORT,
35 | port_next_id: vessel.NEXT_PORT_ID,
36 | port_next: vessel.NEXT_PORT_NAME,
37 | }));
38 | };
39 |
40 | const fetchVesselsInArea: Function = (regions = ["WMED", "EMED"], cb) => {
41 | const timeframe = [60, 525600];
42 | fetchResultByArea(regions.join(","), timeframe.join(","), (result) => {
43 | if (!result?.data.length) {
44 | return cb(null);
45 | }
46 | return cb(mapResult(result));
47 | });
48 | };
49 |
50 | const fetchResultNearMe = async (lat, lng, distance, time, cb) => {
51 | await scraper.fetch(
52 | {
53 | 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}`,
54 | referer:
55 | "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",
56 | responseSelector: "/en/reports?",
57 | extraHeaders: {
58 | "vessel-image": "0053e92efe9e7772299d24de2d0985adea14",
59 | },
60 | },
61 | cb,
62 | );
63 | };
64 |
65 | const fetchVesselsNearMe: Function = (
66 | lat = 51.7419,
67 | lng = 3.89773,
68 | distance = 2,
69 | cb,
70 | ) => {
71 | const timeframe = [60, 525600];
72 | fetchResultNearMe(lat, lng, distance, timeframe.join(","), (result: any) => {
73 | if (!result?.data.length) {
74 | return cb(null);
75 | }
76 | return cb(mapResult(result));
77 | });
78 | };
79 |
80 | export class areaApi {
81 | static fetchVesselsInArea = fetchVesselsInArea;
82 | static fetchVesselsNearMe = fetchVesselsNearMe;
83 | }
84 |
--------------------------------------------------------------------------------
/src/legacy/lib/puppeteer.ts:
--------------------------------------------------------------------------------
1 | const puppeteer = require("puppeteer");
2 | const scrapeJsonFromResponse = async (options, cb) => {
3 | const browser = await puppeteer.launch({
4 | args: [
5 | // Required for Docker version of Puppeteer
6 | "--no-sandbox",
7 | "--disable-setuid-sandbox",
8 | // This will write shared memory files into /tmp instead of /dev/shm,
9 | // because Docker’s default for /dev/shm is 64MB
10 | "--disable-dev-shm-usage",
11 | ],
12 | });
13 | const page = await browser.newPage();
14 | await page.setExtraHTTPHeaders({
15 | "x-requested-with": "XMLHttpRequest",
16 | referer: options.referer,
17 | ...options.extraHeaders,
18 | });
19 | page.on("request", (interceptedRequest) => {
20 | const reqUrl = interceptedRequest.url();
21 | console.log("A request was started: ", reqUrl);
22 | });
23 | page.on("requestfinished", async (request) => {
24 | const resUrl = request.url();
25 | if (resUrl.indexOf(options.responseSelector) !== -1) {
26 | const response = request.response();
27 | const json = await response.json();
28 | console.log("A response was received: ", await response.url());
29 | cb(json);
30 | }
31 | });
32 | // Mock real desktop chrome
33 | page.setViewport({
34 | height: 1302,
35 | width: 2458,
36 | });
37 | page.setUserAgent(
38 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
39 | );
40 | await page.evaluateOnNewDocument(() => {
41 | Object.defineProperty(navigator, "languages", {
42 | get: function () {
43 | return ["en-US", "en", "de-DE"];
44 | },
45 | });
46 | Object.defineProperty(navigator, "plugins", {
47 | get: function () {
48 | // this just needs to have `length > 0`, but we could mock the plugins too
49 | return [1, 2, 3, 4, 5];
50 | },
51 | });
52 | });
53 |
54 | await page.goto(options.url, { waitUntil: "networkidle0" });
55 |
56 | await browser.close();
57 | };
58 |
59 | module.exports = {
60 | fetch: scrapeJsonFromResponse,
61 | };
62 |
--------------------------------------------------------------------------------
/src/static/index.html:
--------------------------------------------------------------------------------
1 | locationsource
2 | Solution to access machine readable AIS Data. This solution uses the free web solutions to crawl the data and returns them in json.
3 | How to use
4 | Paths
5 | /:sourcetype/:source/:vehicleidentifier/location/latest
6 | find latest position for vehicle for specified sourcetype and source
7 | example: http://localhost:5000/ais/mt/211281610/location/latest
8 | /:source/:placeidentifier/vehicles/
9 | /:source/area/vehicles
10 | Legacy Paths
11 | As this repo is a successor to the ais-api, some legacy paths are added. Those will be removed in future versions
12 | /legacy/getLastPosition/:mmsi
13 | Takes position from MT and from VT and returns the newest
14 | example: http://localhost:5000/legacy/getLastPosition/211281610
15 | /legacy/getLastPositionFromVF/:mmsi
16 | Returns position from VF
17 | example: http://localhost:5000/legacy/getLastPositionFromVF/211281610
18 | /legacy/getLastPositionFromMT/:mmsi
19 | Returns position from MT
20 | example: http://localhost:5000/legacy/getLastPositionFromMT/211281610
21 | /legacy/getVesselsInArea/:area
22 | Returns all vessels in area, defined by a list of area keywords
23 | example: http://localhost:5000/legacy/getVesselsInArea/WMED,EMED
24 | /legacy/getVesselsNearMe/:lat/:lng/:distance
25 | Returns all vessels near me, defined by a location in latitude, longitude, and distance
26 | example: http://localhost:5000/legacy/getVesselsNearMe/51.74190/3.89773/2
27 | [{
28 | name: vessel.SHIPNAME,
29 | id: vessel.SHIP_ID,
30 | lat: Number(vessel.LAT),
31 | lon: Number(vessel.LON),
32 | timestamp: vessel.LAST_POS,
33 | mmsi: vessel.MMSI,
34 | imo: vessel.IMO,
35 | callsign: vessel.CALLSIGN,
36 | speed: Number(vessel.SPEED),
37 | area: vessel.AREA_CODE,
38 | type: vessel.TYPE_SUMMARY,
39 | country: vessel.COUNTRY,
40 | destination: vessel.DESTINATION,
41 | port_current_id: vessel.PORT_ID,
42 | port_current: vessel.CURRENT_PORT,
43 | port_next_id: vessel.NEXT_PORT_ID,
44 | port_next: vessel.NEXT_PORT_NAME,
45 | },…]
46 |
47 | /legacy/getVesselsInPort/:shipPort
48 | Returns all vessels in a port, named after the MT nomenclature
49 | example: http://localhost:5000/legacy/getVesselsInPort/piraeus
50 | Output format identical to getVesselsInArea
51 | Install on local machine
52 | Requirements: npm & nodejs.
53 |
54 | clone this repo
55 |
56 | run npm install
57 |
58 | run npm run dev
59 |
60 |
61 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 | /* Language and Environment */
13 | "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
14 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
15 | // "jsx": "preserve", /* Specify what JSX code is generated. */
16 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
17 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
18 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
19 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
20 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
21 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
22 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
23 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
24 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
25 |
26 | /* Modules */
27 | "module": "commonjs" /* Specify what module code is generated. */,
28 | "rootDir": "src" /* Specify the root folder within your source files. */,
29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
33 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
36 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
37 | // "resolveJsonModule": true, /* Enable importing .json files. */
38 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
39 |
40 | /* JavaScript Support */
41 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
42 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
43 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
44 |
45 | /* Emit */
46 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
47 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
48 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
49 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
50 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
51 | "outDir": "./dist" /* Specify an output folder for all emitted files. */,
52 | // "removeComments": true, /* Disable emitting comments. */
53 | // "noEmit": true, /* Disable emitting files from a compilation. */
54 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
55 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
56 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
57 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
59 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
60 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
61 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
62 | // "newLine": "crlf", /* Set the newline character for emitting files. */
63 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
64 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
65 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
66 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
67 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
68 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
69 |
70 | /* Interop Constraints */
71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
72 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
73 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
74 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
75 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
76 |
77 | /* Type Checking */
78 | "strict": true /* Enable all strict type-checking options. */,
79 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
80 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
81 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
82 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
83 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
84 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
85 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
86 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
87 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
88 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
89 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
90 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
91 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
92 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
93 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
94 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
95 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
96 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
97 |
98 | /* Completeness */
99 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
100 | "skipLibCheck": true /* Skip type checking all .d.ts files. */,
101 | "lib": ["dom"]
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
|