import type { ZoneId } from "../../zone/model/meta"; import { haversineNm } from "../../../shared/lib/geo/haversineNm"; import { VESSEL_TYPES } from "./meta"; import type { FleetOwner, FleetState, TrawlPair, Vessel, VesselTypeCode } from "./types"; const SURNAMES = ["张", "王", "李", "刘", "陈", "杨", "黄", "赵", "周", "吴", "徐", "孙", "马", "朱", "胡", "郭", "林", "何", "高", "罗"]; const REGIONS = ["荣成", "石岛", "烟台", "威海", "日照", "青岛", "连云港", "舟山", "象山", "大连"]; const ZONE_BOUNDS: Record = { "1": { lon: [128.85, 131.70], lat: [36.16, 38.25] }, "2": { lon: [126.00, 128.90], lat: [32.18, 34.35] }, "3": { lon: [124.12, 126.06], lat: [33.13, 35.00] }, "4": { lon: [124.33, 125.85], lat: [35.00, 37.00] }, }; function rnd(a: number, b: number) { return a + Math.random() * (b - a); } function pick(arr: readonly T[]) { return arr[Math.floor(Math.random() * arr.length)]; } function randomPointInZone(zone: ZoneId) { const b = ZONE_BOUNDS[zone]; // Small margin to avoid sitting exactly on the edge. const lat = rnd(b.lat[0] + 0.05, b.lat[1] - 0.05); const lon = rnd(b.lon[0] + 0.05, b.lon[1] - 0.05); return { lat, lon }; } function makePermit(id: number, suffix: "A" | "B") { return `C21-${10000 + id}${suffix}`; } export function createMockFleetState(): FleetState { const vessels: Vessel[] = []; const owners: FleetOwner[] = []; const ptPairs: TrawlPair[] = []; let vid = 1; // PT pairs: PT count == PT-S count, treat as "pair count". for (let i = 0; i < VESSEL_TYPES.PT.count; i++) { const owner = `${pick(SURNAMES)}${pick(SURNAMES)}${pick(["渔业", "海产", "水产", "船务"])}${pick(["有限公司", "合作社", ""])}`; const region = pick(REGIONS); const zone = pick(["2", "3"]); const { lat, lon } = randomPointInZone(zone); const isFishing = Math.random() < 0.55; const sp = isFishing ? rnd(2.5, 4.5) : rnd(6, 11); const crs = rnd(0, 360); const pairDist = isFishing ? rnd(0.2, 1.2) : rnd(0, 0.3); // NM (rough) const pairAngle = rnd(0, 360); const lat2 = lat + (pairDist / 60) * Math.cos((pairAngle * Math.PI) / 180); const lon2 = lon + ((pairDist / 60) * Math.sin((pairAngle * Math.PI) / 180)) / Math.cos((lat * Math.PI) / 180); const permitBase = vid; const ptId = vid++; const ptsId = vid++; const pt: Vessel = { id: ptId, permit: makePermit(permitBase, "A"), code: "PT", color: VESSEL_TYPES.PT.color, lat, lon, speed: Number(sp.toFixed(1)), course: Number(crs.toFixed(0)), state: isFishing ? "조업중" : sp < 1 ? "정박" : "항해중", zone, isFishing, owner, region, pairId: null, pairDistNm: Number(pairDist.toFixed(2)), nearVesselIds: [], }; const pts: Vessel = { id: ptsId, permit: makePermit(permitBase, "B"), code: "PT-S", color: VESSEL_TYPES["PT-S"].color, lat: Number(lat2.toFixed(4)), lon: Number(lon2.toFixed(4)), speed: Number((sp + rnd(-0.3, 0.3)).toFixed(1)), course: Number((crs + rnd(-10, 10)).toFixed(0)), state: isFishing ? "조업중" : sp < 1 ? "정박" : "항해중", zone, isFishing, owner, region, pairId: null, pairDistNm: pt.pairDistNm, nearVesselIds: [], }; pt.pairId = pts.id; pts.pairId = pt.id; vessels.push(pt, pts); ptPairs.push({ mainId: pt.id, subId: pts.id, owner, region }); owners.push({ name: owner, region, vessels: [pt.id, pts.id], type: "trawl" }); } // GN vessels for (let i = 0; i < VESSEL_TYPES.GN.count; i++) { const attachToOwner = Math.random() < 0.3 ? owners[Math.floor(Math.random() * owners.length)] : null; const owner = attachToOwner ? attachToOwner.name : `${pick(SURNAMES)}${pick(SURNAMES)}${pick(["渔业", "水产"])}有限公司`; const region = attachToOwner ? attachToOwner.region : pick(REGIONS); const zone = pick(["2", "3", "4"]); const { lat, lon } = randomPointInZone(zone); const isFishing = Math.random() < 0.5; const sp = isFishing ? rnd(0.5, 2) : rnd(5, 10); const id = vid++; const v: Vessel = { id, permit: makePermit(id, "A"), code: "GN", color: VESSEL_TYPES.GN.color, lat, lon, speed: Number(sp.toFixed(1)), course: Number(rnd(0, 360).toFixed(0)), state: isFishing ? pick(["표류", "투망", "양망"]) : sp < 1 ? "정박" : "항해중", zone, isFishing, owner, region, pairId: null, pairDistNm: null, nearVesselIds: [], }; vessels.push(v); if (attachToOwner) attachToOwner.vessels.push(v.id); else owners.push({ name: owner, region, vessels: [v.id], type: "gn" }); } // OT for (let i = 0; i < VESSEL_TYPES.OT.count; i++) { const owner = `${pick(SURNAMES)}${pick(SURNAMES)}远洋渔业`; const region = pick(REGIONS); const zone = pick(["2", "3"]); const { lat, lon } = randomPointInZone(zone); const isFishing = Math.random() < 0.5; const sp = isFishing ? rnd(2.5, 5) : rnd(5, 10); const id = vid++; const v: Vessel = { id, permit: makePermit(id, "A"), code: "OT", color: VESSEL_TYPES.OT.color, lat, lon, speed: Number(sp.toFixed(1)), course: Number(rnd(0, 360).toFixed(0)), state: isFishing ? "조업중" : "항해중", zone, isFishing, owner, region, pairId: null, pairDistNm: null, nearVesselIds: [], }; vessels.push(v); owners.push({ name: owner, region, vessels: [v.id], type: "ot" }); } // PS for (let i = 0; i < VESSEL_TYPES.PS.count; i++) { const owner = `${pick(SURNAMES)}${pick(SURNAMES)}水产`; const region = pick(REGIONS); const zone = pick(["1", "2", "3", "4"]); const { lat, lon } = randomPointInZone(zone); const isFishing = Math.random() < 0.5; const sp = isFishing ? rnd(0.3, 1.5) : rnd(5, 9); const id = vid++; const v: Vessel = { id, permit: makePermit(id, "A"), code: "PS", color: VESSEL_TYPES.PS.color, lat, lon, speed: Number(sp.toFixed(1)), course: Number(rnd(0, 360).toFixed(0)), state: isFishing ? pick(["위망", "채낚기"]) : "항해중", zone, isFishing, owner, region, pairId: null, pairDistNm: null, nearVesselIds: [], }; vessels.push(v); owners.push({ name: owner, region, vessels: [v.id], type: "ps" }); } // FC — assigned to trawl owners (positioned near PT) const trawlOwners = owners.filter((o) => o.type === "trawl"); for (let i = 0; i < VESSEL_TYPES.FC.count; i++) { const oi = i < trawlOwners.length ? trawlOwners[i] : pick(trawlOwners); const refId = oi.vessels.find((id) => vessels[id - 1]?.code === "PT") ?? oi.vessels[0]; const ref = vessels[refId - 1]; const zone = pick(["2", "3"]); const lat = ref.lat + rnd(-0.2, 0.2); const lon = ref.lon + rnd(-0.2, 0.2); const isNear = Math.random() < 0.4; const sp = isNear ? rnd(0.5, 1.5) : rnd(5, 9); const nearVesselIds = isNear ? oi.vessels.filter((id) => vessels[id - 1]?.code !== "FC").slice(0, 2) : []; const v: Vessel = { id: vid, permit: makePermit(vid, "A"), code: "FC", color: VESSEL_TYPES.FC.color, lat, lon, speed: Number(sp.toFixed(1)), course: Number(rnd(0, 360).toFixed(0)), state: isNear ? "환적" : "항해중", zone, isFishing: isNear, // kept from prototype: treat "환적" as fishing-like activity owner: oi.name, region: oi.region, pairId: null, pairDistNm: null, nearVesselIds, }; vid += 1; vessels.push(v); oi.vessels.push(v.id); } // Ensure initial pair distances are consistent with actual coordinates. for (const p of ptPairs) { const a = vessels[p.mainId - 1]; const b = vessels[p.subId - 1]; const d = haversineNm(a.lat, a.lon, b.lat, b.lon); a.pairDistNm = d; b.pairDistNm = d; } return { vessels, owners, ptPairs }; } export function tickMockFleetState(state: FleetState) { for (const v of state.vessels) { v.lat += (0.5 - Math.random()) * 0.003; v.lon += (0.5 - Math.random()) * 0.003; v.speed = Math.max(0, Number((v.speed + (0.5 - Math.random()) * 0.4).toFixed(1))); v.course = Number(((v.course + (0.5 - Math.random()) * 6 + 360) % 360).toFixed(0)); } for (const p of state.ptPairs) { const a = state.vessels[p.mainId - 1]; const b = state.vessels[p.subId - 1]; const d = haversineNm(a.lat, a.lon, b.lat, b.lon); a.pairDistNm = d; b.pairDistNm = d; } } export function isVesselCode(code: string): code is VesselTypeCode { return code === "PT" || code === "PT-S" || code === "GN" || code === "OT" || code === "PS" || code === "FC"; }