Skip to content

Commit a1efca1

Browse files
committed
Reclip adjusted curvature samples
1 parent a41b5fc commit a1efca1

2 files changed

Lines changed: 83 additions & 164 deletions

File tree

src/lib/line-of-sight.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,23 @@ describe("line of sight helpers", () => {
138138
expect(adjusted.at(-1)).toEqual({ x: 10_000, y: 10 });
139139
});
140140

141+
it("clips sparse curvature-adjusted line-of-sight samples after densifying terrain", () => {
142+
const adjusted = buildVisibleLineOfSightSegments(
143+
[point(0, 9), point(10_000, 9)],
144+
{ startElevation: 10, endElevation: 10 },
145+
"curvature-adjusted",
146+
);
147+
const midpoint = adjusted.find(
148+
(sample): sample is { x: number; y: number } =>
149+
sample !== null && sample.x === 5_000,
150+
);
151+
152+
expect(adjusted[0]).toEqual({ x: 0, y: 10 });
153+
expect(adjusted).toContain(null);
154+
expect(adjusted.at(-1)).toEqual({ x: 10_000, y: 10 });
155+
expect(midpoint).toBeUndefined();
156+
});
157+
141158
it("adds interior samples to render sparse curvature-adjusted line of sight as a curve", () => {
142159
const adjusted = buildVisibleLineOfSightSegments(
143160
[point(0, 0), point(10_000, 0)],
@@ -317,6 +334,24 @@ describe("line of sight helpers", () => {
317334
expect(adjustedSegments.upper).toHaveLength(1);
318335
});
319336

337+
it("clips sparse curvature-adjusted Fresnel boundary samples after densifying terrain", () => {
338+
const adjustedSegments = buildVisibleFresnelZoneShellSegments(
339+
[point(0, -2), point(10_000, -2)],
340+
{ startElevation: 10, endElevation: 10 },
341+
5800,
342+
1,
343+
undefined,
344+
"curvature-adjusted",
345+
);
346+
const lowerMidpoint = adjustedSegments.lower
347+
.flat()
348+
.find((sample) => sample.x === 5_000);
349+
350+
expect(adjustedSegments.lower).toHaveLength(2);
351+
expect(adjustedSegments.upper).toHaveLength(1);
352+
expect(lowerMidpoint).toBeUndefined();
353+
});
354+
320355
it("adds interior samples to render sparse curvature-adjusted Fresnel boundaries as curves", () => {
321356
const adjustedSegments = buildVisibleFresnelZoneShellSegments(
322357
[point(0, -10), point(10_000, -10)],

src/lib/line-of-sight.ts

Lines changed: 48 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,13 @@ export function buildVisibleLineOfSightSegments(
305305
if (!endpoints || points.length === 0) return [];
306306

307307
const lastDistance = points.at(-1)?.distance ?? 0;
308+
const profilePoints =
309+
adjustment === "curvature-adjusted"
310+
? densifyProfilePointsForCurvature(points, lastDistance)
311+
: points;
308312
const output: (LineOfSightChartPoint | null)[] = [];
309313
const first = classifyPoint(
310-
points[0]!,
314+
profilePoints[0]!,
311315
lastDistance,
312316
endpoints,
313317
adjustment,
@@ -317,16 +321,16 @@ export function buildVisibleLineOfSightSegments(
317321

318322
appendLineOfSightPoint(output, first.visible ? first.linePoint : null);
319323

320-
for (let index = 1; index < points.length; index++) {
324+
for (let index = 1; index < profilePoints.length; index++) {
321325
const previous = classifyPoint(
322-
points[index - 1]!,
326+
profilePoints[index - 1]!,
323327
lastDistance,
324328
endpoints,
325329
adjustment,
326330
unitScales,
327331
);
328332
const current = classifyPoint(
329-
points[index]!,
333+
profilePoints[index]!,
330334
lastDistance,
331335
endpoints,
332336
adjustment,
@@ -351,9 +355,7 @@ export function buildVisibleLineOfSightSegments(
351355
appendLineOfSightPoint(output, current.visible ? current.linePoint : null);
352356
}
353357

354-
return adjustment === "curvature-adjusted"
355-
? densifyLineOfSightPoints(output, lastDistance, endpoints, unitScales)
356-
: output;
358+
return output;
357359
}
358360

359361
type ClassifiedPoint = {
@@ -507,8 +509,12 @@ function buildVisibleFresnelBoundarySegments(
507509
adjustment: LineOfSightAdjustment,
508510
boundary: FresnelBoundary,
509511
): LineOfSightChartPoint[][] {
512+
const profilePoints =
513+
adjustment === "curvature-adjusted"
514+
? densifyProfilePointsForCurvature(points, totalDistance)
515+
: points;
510516
const first = classifyFresnelBoundaryPoint(
511-
points[0]!,
517+
profilePoints[0]!,
512518
endpoints,
513519
totalDistance,
514520
frequencyMhz,
@@ -523,9 +529,9 @@ function buildVisibleFresnelBoundarySegments(
523529
const output: (LineOfSightChartPoint | null)[] = [];
524530
appendLineOfSightPoint(output, first.visible ? first.linePoint : null);
525531

526-
for (let index = 1; index < points.length; index++) {
532+
for (let index = 1; index < profilePoints.length; index++) {
527533
const previous = classifyFresnelBoundaryPoint(
528-
points[index - 1]!,
534+
profilePoints[index - 1]!,
529535
endpoints,
530536
totalDistance,
531537
frequencyMhz,
@@ -535,7 +541,7 @@ function buildVisibleFresnelBoundarySegments(
535541
boundary,
536542
);
537543
const current = classifyFresnelBoundaryPoint(
538-
points[index]!,
544+
profilePoints[index]!,
539545
endpoints,
540546
totalDistance,
541547
frequencyMhz,
@@ -566,19 +572,7 @@ function buildVisibleFresnelBoundarySegments(
566572
appendLineOfSightPoint(output, current.visible ? current.linePoint : null);
567573
}
568574

569-
const segments = splitLineOfSightSegments(output);
570-
571-
return adjustment === "curvature-adjusted"
572-
? densifyFresnelBoundarySegments(
573-
segments,
574-
endpoints,
575-
totalDistance,
576-
frequencyMhz,
577-
shellNumber,
578-
unitScales,
579-
boundary,
580-
)
581-
: segments;
575+
return splitLineOfSightSegments(output);
582576
}
583577

584578
function classifyFresnelBoundaryPoint(
@@ -640,136 +634,61 @@ function fresnelBoundaryCrossingPoint(
640634
return { x: distance, y: elevation };
641635
}
642636

643-
function densifyLineOfSightPoints(
644-
points: (LineOfSightChartPoint | null)[],
645-
totalDistance: number,
646-
endpoints: LineOfSightEndpoints,
647-
unitScales: FresnelZoneUnitScales,
648-
): (LineOfSightChartPoint | null)[] {
649-
return densifyNullableChartPoints(points, totalDistance, (distance) =>
650-
lineOfSightChartPointAt(
651-
distance,
652-
totalDistance,
653-
endpoints,
654-
"curvature-adjusted",
655-
unitScales,
656-
),
657-
);
658-
}
659-
660-
function densifyFresnelBoundarySegments(
661-
segments: LineOfSightChartPoint[][],
662-
endpoints: LineOfSightEndpoints,
663-
totalDistance: number,
664-
frequencyMhz: number,
665-
shellNumber: number,
666-
unitScales: FresnelZoneUnitScales,
667-
boundary: FresnelBoundary,
668-
): LineOfSightChartPoint[][] {
669-
return segments.map((segment) =>
670-
densifyChartSegment(segment, totalDistance, (distance) =>
671-
fresnelBoundaryChartPointAt(
672-
distance,
673-
endpoints,
674-
totalDistance,
675-
frequencyMhz,
676-
shellNumber,
677-
unitScales,
678-
boundary,
679-
),
680-
),
681-
);
682-
}
683-
684-
function densifyNullableChartPoints(
685-
points: (LineOfSightChartPoint | null)[],
637+
function densifyProfilePointsForCurvature(
638+
points: ProfilePoint[],
686639
totalDistance: number,
687-
pointAt: (distance: number) => LineOfSightChartPoint | null,
688-
): (LineOfSightChartPoint | null)[] {
689-
const output: (LineOfSightChartPoint | null)[] = [];
690-
let previous: LineOfSightChartPoint | null = null;
691-
692-
for (const point of points) {
693-
if (point === null) {
694-
appendLineOfSightPoint(output, null);
695-
previous = null;
696-
continue;
697-
}
698-
699-
if (previous) {
700-
appendInteriorSamples(
701-
output,
702-
previous.x,
703-
point.x,
704-
totalDistance,
705-
pointAt,
706-
);
707-
}
640+
): ProfilePoint[] {
641+
if (!isPositiveFinite(totalDistance) || points.length < 2) return points;
708642

709-
appendLineOfSightPoint(output, point);
710-
previous = point;
711-
}
643+
const output: ProfilePoint[] = [];
712644

713-
return output;
714-
}
715-
716-
function densifyChartSegment(
717-
segment: LineOfSightChartPoint[],
718-
totalDistance: number,
719-
pointAt: (distance: number) => LineOfSightChartPoint | null,
720-
): LineOfSightChartPoint[] {
721-
const output: LineOfSightChartPoint[] = [];
645+
for (let index = 0; index < points.length; index++) {
646+
const point = points[index];
647+
if (!point) continue;
722648

723-
for (const point of segment) {
724-
const previous = output.at(-1) ?? null;
649+
output.push(point);
725650

726-
if (previous) {
727-
appendInteriorSamples(
728-
output,
729-
previous.x,
730-
point.x,
731-
totalDistance,
732-
pointAt,
733-
);
734-
}
651+
const nextPoint = points[index + 1];
652+
if (!nextPoint) continue;
735653

736-
output.push(point);
654+
appendInteriorProfilePoints(output, point, nextPoint, totalDistance);
737655
}
738656

739657
return output;
740658
}
741659

742-
function appendInteriorSamples(
743-
output: (LineOfSightChartPoint | null)[],
744-
startDistance: number,
745-
endDistance: number,
660+
function appendInteriorProfilePoints(
661+
output: ProfilePoint[],
662+
start: ProfilePoint,
663+
end: ProfilePoint,
746664
totalDistance: number,
747-
pointAt: (distance: number) => LineOfSightChartPoint | null,
748665
): void {
749666
if (
750-
!isPositiveFinite(totalDistance) ||
751-
!Number.isFinite(startDistance) ||
752-
!Number.isFinite(endDistance) ||
753-
startDistance === endDistance
667+
!Number.isFinite(start.distance) ||
668+
!Number.isFinite(end.distance) ||
669+
!Number.isFinite(start.elevation) ||
670+
!Number.isFinite(end.elevation) ||
671+
start.distance === end.distance
754672
) {
755673
return;
756674
}
757675

758-
const distanceDelta = endDistance - startDistance;
676+
const distanceDelta = end.distance - start.distance;
759677
const sampleStep = totalDistance / curvatureAdjustedSampleCount;
760678
const sampleCount = Math.max(
761679
1,
762680
Math.ceil(Math.abs(distanceDelta) / sampleStep),
763681
);
764682

765683
for (let sampleIndex = 1; sampleIndex < sampleCount; sampleIndex++) {
766-
const distance =
767-
startDistance + (distanceDelta * sampleIndex) / sampleCount;
768-
const point = pointAt(distance);
769-
770-
if (point) {
771-
output.push(point);
772-
}
684+
const ratio = sampleIndex / sampleCount;
685+
686+
output.push({
687+
distance: start.distance + distanceDelta * ratio,
688+
elevation: start.elevation! + (end.elevation! - start.elevation!) * ratio,
689+
x: start.x + (end.x - start.x) * ratio,
690+
y: start.y + (end.y - start.y) * ratio,
691+
});
773692
}
774693
}
775694

@@ -791,41 +710,6 @@ function lineOfSightChartPointAt(
791710
return elevation === null ? null : { x: distance, y: elevation };
792711
}
793712

794-
function fresnelBoundaryChartPointAt(
795-
distance: number,
796-
endpoints: LineOfSightEndpoints,
797-
totalDistance: number,
798-
frequencyMhz: number,
799-
shellNumber: number,
800-
unitScales: FresnelZoneUnitScales,
801-
boundary: FresnelBoundary,
802-
): LineOfSightChartPoint | null {
803-
const radius = fresnelRadiusForChartDistance(
804-
distance,
805-
totalDistance,
806-
frequencyMhz,
807-
shellNumber,
808-
unitScales,
809-
);
810-
const centerElevation = lineOfSightElevationForAdjustment(
811-
distance,
812-
totalDistance,
813-
endpoints,
814-
"curvature-adjusted",
815-
unitScales,
816-
);
817-
818-
if (radius === null || centerElevation === null) return null;
819-
820-
return {
821-
x: distance,
822-
y:
823-
boundary === "upper"
824-
? centerElevation + radius
825-
: centerElevation - radius,
826-
};
827-
}
828-
829713
function lineOfSightElevationForAdjustment(
830714
distance: number,
831715
lastDistance: number,

0 commit comments

Comments
 (0)