163 lines
4.9 KiB
TypeScript
163 lines
4.9 KiB
TypeScript
import { fetchVesselTrack } from '../../../entities/vesselTrack/api/fetchTrack';
|
|
import { convertLegacyTrackPointsToProcessedTrack } from '../lib/adapters';
|
|
import type { ProcessedTrack } from '../model/track.types';
|
|
|
|
type QueryTrackByMmsiParams = {
|
|
mmsi: number;
|
|
minutes: number;
|
|
shipNameHint?: string;
|
|
shipKindCodeHint?: string;
|
|
nationalCodeHint?: string;
|
|
};
|
|
|
|
type V2TrackResponse = {
|
|
vesselId?: string;
|
|
targetId?: string;
|
|
sigSrcCd?: string;
|
|
shipName?: string;
|
|
shipKindCode?: string;
|
|
nationalCode?: string;
|
|
geometry?: [number, number][];
|
|
timestamps?: Array<string | number>;
|
|
speeds?: number[];
|
|
totalDistance?: number;
|
|
avgSpeed?: number;
|
|
maxSpeed?: number;
|
|
pointCount?: number;
|
|
};
|
|
|
|
function normalizeTimestampMs(value: string | number): number {
|
|
if (typeof value === 'number') return value < 1e12 ? value * 1000 : value;
|
|
if (/^\d{10,}$/.test(value)) {
|
|
const asNum = Number(value);
|
|
return asNum < 1e12 ? asNum * 1000 : asNum;
|
|
}
|
|
const parsed = new Date(value).getTime();
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
function toFiniteNumber(value: unknown): number | null {
|
|
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
if (typeof value === 'string') {
|
|
const parsed = Number(value.trim());
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function convertV2Tracks(rows: V2TrackResponse[]): ProcessedTrack[] {
|
|
const out: ProcessedTrack[] = [];
|
|
|
|
for (const row of rows) {
|
|
if (!row.geometry || row.geometry.length === 0) continue;
|
|
const timestamps = Array.isArray(row.timestamps) ? row.timestamps : [];
|
|
const timestampsMs = timestamps.map((ts) => normalizeTimestampMs(ts));
|
|
|
|
const sortedIndices = timestampsMs
|
|
.map((_, idx) => idx)
|
|
.sort((a, b) => timestampsMs[a] - timestampsMs[b]);
|
|
|
|
const geometry: [number, number][] = [];
|
|
const sortedTimes: number[] = [];
|
|
const speeds: number[] = [];
|
|
for (const idx of sortedIndices) {
|
|
const coord = row.geometry?.[idx];
|
|
if (!Array.isArray(coord) || coord.length !== 2) continue;
|
|
const nLon = toFiniteNumber(coord[0]);
|
|
const nLat = toFiniteNumber(coord[1]);
|
|
if (nLon == null || nLat == null) continue;
|
|
|
|
geometry.push([nLon, nLat]);
|
|
sortedTimes.push(timestampsMs[idx]);
|
|
speeds.push(toFiniteNumber(row.speeds?.[idx]) ?? 0);
|
|
}
|
|
|
|
if (geometry.length === 0) continue;
|
|
|
|
const targetId = row.targetId || row.vesselId || '';
|
|
const sigSrcCd = row.sigSrcCd || '000001';
|
|
|
|
out.push({
|
|
vesselId: row.vesselId || `${sigSrcCd}_${targetId}`,
|
|
targetId,
|
|
sigSrcCd,
|
|
shipName: (row.shipName || '').trim() || targetId,
|
|
shipKindCode: row.shipKindCode || '000027',
|
|
nationalCode: row.nationalCode || '',
|
|
geometry,
|
|
timestampsMs: sortedTimes,
|
|
speeds,
|
|
stats: {
|
|
totalDistanceNm: row.totalDistance || 0,
|
|
avgSpeed: row.avgSpeed || 0,
|
|
maxSpeed: row.maxSpeed || 0,
|
|
pointCount: row.pointCount || geometry.length,
|
|
},
|
|
});
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
async function queryLegacyTrack(params: QueryTrackByMmsiParams): Promise<ProcessedTrack[]> {
|
|
const response = await fetchVesselTrack(params.mmsi, params.minutes);
|
|
if (!response.success || response.data.length === 0) return [];
|
|
|
|
const converted = convertLegacyTrackPointsToProcessedTrack(params.mmsi, response.data, {
|
|
shipName: params.shipNameHint,
|
|
shipKindCode: params.shipKindCodeHint,
|
|
nationalCode: params.nationalCodeHint,
|
|
});
|
|
|
|
return converted ? [converted] : [];
|
|
}
|
|
|
|
async function queryV2Track(params: QueryTrackByMmsiParams): Promise<ProcessedTrack[]> {
|
|
const base = (import.meta.env.VITE_TRACK_V2_BASE_URL || '').trim();
|
|
if (!base) {
|
|
return queryLegacyTrack(params);
|
|
}
|
|
|
|
const end = new Date();
|
|
const start = new Date(end.getTime() - params.minutes * 60_000);
|
|
|
|
const requestBody = {
|
|
startTime: start.toISOString().slice(0, 19),
|
|
endTime: end.toISOString().slice(0, 19),
|
|
vessels: [{ sigSrcCd: '000001', targetId: String(params.mmsi) }],
|
|
isIntegration: '0',
|
|
};
|
|
|
|
const endpoint = `${base.replace(/\/$/, '')}/api/v2/tracks/vessels`;
|
|
const res = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', accept: 'application/json' },
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
return queryLegacyTrack(params);
|
|
}
|
|
|
|
const json = (await res.json()) as unknown;
|
|
const rows = Array.isArray(json)
|
|
? (json as V2TrackResponse[])
|
|
: Array.isArray((json as { data?: unknown }).data)
|
|
? ((json as { data: V2TrackResponse[] }).data)
|
|
: [];
|
|
|
|
const converted = convertV2Tracks(rows);
|
|
if (converted.length > 0) return converted;
|
|
return queryLegacyTrack(params);
|
|
}
|
|
|
|
export async function queryTrackByMmsi(params: QueryTrackByMmsiParams): Promise<ProcessedTrack[]> {
|
|
const mode = String(import.meta.env.VITE_TRACK_SOURCE_MODE || 'legacy').toLowerCase();
|
|
|
|
if (mode === 'v2') {
|
|
return queryV2Track(params);
|
|
}
|
|
|
|
return queryLegacyTrack(params);
|
|
}
|