up
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { gridToLatLon, gridSquareBounds, greatCirclePoints, pathBetween } from '@/lib/maidenhead';
|
||||
import { gridToLatLon, gridSquareBounds, greatCirclePoints, pathBetween, destinationPoint } from '@/lib/maidenhead';
|
||||
|
||||
// MainMap — Log4OM-style dual map for the Main tab:
|
||||
// • Left: a world map with the great-circle path drawn from the operator to
|
||||
@@ -15,6 +15,26 @@ interface Props {
|
||||
toGrid: string; // contacted-station grid
|
||||
fromLabel?: string; // operator callsign
|
||||
toLabel?: string; // DX callsign
|
||||
beamAzimuths?: number[]; // antenna heading(s) (deg) → draw a beam lobe each
|
||||
beamWidth?: number; // beamwidth (deg), default 30
|
||||
}
|
||||
|
||||
// unwrapLon makes a lat/lon ring continuous in longitude (each point within
|
||||
// 180° of the previous) so a polygon crossing the antimeridian doesn't snap
|
||||
// across the whole map. Coords may exceed ±180; Leaflet (worldCopyJump) is fine.
|
||||
function unwrapLon(ring: [number, number][]): [number, number][] {
|
||||
const out: [number, number][] = [];
|
||||
let prev = NaN;
|
||||
for (const [la, lo] of ring) {
|
||||
let lon = lo;
|
||||
if (!Number.isNaN(prev)) {
|
||||
while (lon - prev > 180) lon -= 360;
|
||||
while (lon - prev < -180) lon += 360;
|
||||
}
|
||||
prev = lon;
|
||||
out.push([la, lon]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const CARTO_LIGHT = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
||||
@@ -31,7 +51,7 @@ function dot(color: string): L.DivIcon {
|
||||
});
|
||||
}
|
||||
|
||||
export function MainMap({ fromGrid, toGrid, fromLabel, toLabel }: Props) {
|
||||
export function MainMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, beamWidth }: Props) {
|
||||
const worldRef = useRef<HTMLDivElement>(null);
|
||||
const locatorRef = useRef<HTMLDivElement>(null);
|
||||
const worldMap = useRef<L.Map | null>(null);
|
||||
@@ -75,6 +95,37 @@ export function MainMap({ fromGrid, toGrid, fromLabel, toLabel }: Props) {
|
||||
const from = gridToLatLon(fromGrid);
|
||||
const to = gridToLatLon(toGrid);
|
||||
|
||||
// ── Antenna beam lobe(s) (drawn first, under the arc/markers) ──
|
||||
if (from && beamAzimuths && beamAzimuths.length) {
|
||||
const half = (beamWidth ?? 30) / 2;
|
||||
const D = 9000; // lobe length (km)
|
||||
const radial = (b: number): [number, number][] =>
|
||||
Array.from({ length: 14 }, (_, i) => {
|
||||
const d = destinationPoint(from.lat, from.lon, b, (D * (i + 1)) / 14);
|
||||
return [d.lat, d.lon] as [number, number];
|
||||
});
|
||||
for (const az of beamAzimuths) {
|
||||
const arc: [number, number][] = [];
|
||||
for (let b = az - half; b <= az + half + 0.001; b += 2) {
|
||||
const d = destinationPoint(from.lat, from.lon, b, D);
|
||||
arc.push([d.lat, d.lon]);
|
||||
}
|
||||
const ring = unwrapLon([
|
||||
[from.lat, from.lon],
|
||||
...radial(az - half),
|
||||
...arc,
|
||||
...radial(az + half).reverse(),
|
||||
]);
|
||||
L.polygon(ring as L.LatLngExpression[], {
|
||||
color: '#dc2626', weight: 1, opacity: 0.5, fillColor: '#dc2626', fillOpacity: 0.14,
|
||||
}).addTo(wo);
|
||||
// Boresight (dashed centre line).
|
||||
const cl = unwrapLon([[from.lat, from.lon], ...radial(az)]);
|
||||
L.polyline(cl as L.LatLngExpression[], { color: '#dc2626', weight: 1.5, opacity: 0.7, dashArray: '5 4' })
|
||||
.bindTooltip(`Beam ${Math.round(az)}°`, { permanent: false, direction: 'top' }).addTo(wo);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Left: world + great-circle arc ──
|
||||
if (from) L.marker([from.lat, from.lon], { icon: dot('#059669'), title: fromLabel || 'Home' }).addTo(wo);
|
||||
if (to) {
|
||||
@@ -108,7 +159,8 @@ export function MainMap({ fromGrid, toGrid, fromLabel, toLabel }: Props) {
|
||||
lm.setView([from.lat, from.lon], 5);
|
||||
}
|
||||
setTimeout(() => { wm.invalidateSize(); lm.invalidateSize(); }, 0);
|
||||
}, [fromGrid, toGrid, fromLabel, toLabel]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fromGrid, toGrid, fromLabel, toLabel, (beamAzimuths ?? []).map((a) => Math.round(a)).join(','), beamWidth]);
|
||||
|
||||
const path = pathBetween(fromGrid, toGrid);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user