Skip to content

Commit b0131a9

Browse files
authored
Merge pull request #6 from dbech/t3code/7d116f6f
Add Fresnel zone overlays
2 parents 68bbfc2 + ff5a58c commit b0131a9

9 files changed

Lines changed: 1014 additions & 15 deletions

File tree

electron/raster/gdal-loader.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ type Stats = {
2222
std_dev?: number;
2323
};
2424

25+
type GdalUnitInfo = {
26+
units?: string;
27+
value?: number;
28+
};
29+
30+
type SpatialReferenceWithUnits = SpatialReference & {
31+
getAngularUnits?: () => GdalUnitInfo;
32+
getLinearUnits?: () => GdalUnitInfo;
33+
};
34+
2535
export async function loadDsmProject(
2636
paths: string[],
2737
): Promise<DsmProjectSummary> {
@@ -76,6 +86,9 @@ export async function loadDsmProject(
7686
projection,
7787
);
7888
const firstNoData = openedFiles[0]!.nodata;
89+
const distanceUnit = distanceUnitForSrs(firstSrs);
90+
const elevationUnit = firstFile.band.unitType || "unknown";
91+
const elevationMetersPerUnit = metersPerElevationUnit(elevationUnit);
7992

8093
const summary: DsmProjectSummary = {
8194
id: projectId,
@@ -91,10 +104,12 @@ export async function loadDsmProject(
91104
),
92105
crsWkt: firstSrs?.toWKT() ?? "",
93106
epsg,
107+
distance: distanceUnit,
94108
extent: projectExtent,
95109
pixelSize: firstFile.pixelSize,
96110
elevation: {
97-
unit: firstFile.band.unitType || "unknown",
111+
unit: elevationUnit,
112+
metersPerUnit: elevationMetersPerUnit,
98113
min: projectMin,
99114
max: projectMax,
100115
...(firstNoData === undefined ? {} : { nodata: firstNoData }),
@@ -208,6 +223,8 @@ function warningsForProject(
208223
sourceSrs: SpatialReference | null,
209224
): string[] {
210225
const warnings: string[] = [];
226+
const horizontalUnit = distanceUnitForSrs(sourceSrs);
227+
const elevationUnit = files[0]?.band.unitType || "unknown";
211228

212229
if (!sourceSrs) {
213230
warnings.push(
@@ -219,6 +236,18 @@ function warningsForProject(
219236
);
220237
}
221238

239+
if (horizontalUnit.metersPerUnit === null) {
240+
warnings.push(
241+
"Fresnel zones are hidden because the DSM horizontal unit cannot be converted to metres.",
242+
);
243+
}
244+
245+
if (metersPerElevationUnit(elevationUnit) === null) {
246+
warnings.push(
247+
"Fresnel zones are hidden because the DSM elevation unit cannot be converted to metres.",
248+
);
249+
}
250+
222251
for (let i = 0; i < files.length; i++) {
223252
for (let j = i + 1; j < files.length; j++) {
224253
if (extentsOverlap(files[i]!.extent, files[j]!.extent)) {
@@ -239,6 +268,57 @@ function warningsForProject(
239268
return warnings;
240269
}
241270

271+
function distanceUnitForSrs(sourceSrs: SpatialReference | null): {
272+
unit: string;
273+
metersPerUnit: number | null;
274+
} {
275+
if (!sourceSrs) return { unit: "unknown", metersPerUnit: null };
276+
277+
const srsWithUnits = sourceSrs as SpatialReferenceWithUnits;
278+
const unitInfo = sourceSrs.isGeographic()
279+
? srsWithUnits.getAngularUnits?.()
280+
: srsWithUnits.getLinearUnits?.();
281+
const unit = unitInfo?.units || "unknown";
282+
const metersPerUnit = sourceSrs.isGeographic()
283+
? null
284+
: finitePositiveOrNull(unitInfo?.value);
285+
286+
return { unit, metersPerUnit };
287+
}
288+
289+
function metersPerElevationUnit(unit: string): number | null {
290+
const normalized = unit.trim().toLowerCase();
291+
if (normalized === "" || normalized === "unknown") return null;
292+
293+
if (["m", "meter", "meters", "metre", "metres"].includes(normalized)) {
294+
return 1;
295+
}
296+
297+
if (
298+
[
299+
"ft",
300+
"foot",
301+
"feet",
302+
"international foot",
303+
"international feet",
304+
"us survey foot",
305+
"us survey feet",
306+
"survey foot",
307+
"survey feet",
308+
].includes(normalized)
309+
) {
310+
return 0.3048;
311+
}
312+
313+
return null;
314+
}
315+
316+
function finitePositiveOrNull(value: number | undefined): number | null {
317+
return typeof value === "number" && Number.isFinite(value) && value > 0
318+
? value
319+
: null;
320+
}
321+
242322
function rasterStats(band: RasterBand): Stats {
243323
try {
244324
const stats = band.computeStatistics(true) as Stats;

electron/raster/project-registry.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ function testProject(id: string, close: () => void): DsmProject {
3636
sourceSrs: null,
3737
summary: {
3838
crsWkt: "",
39-
elevation: { min: 0, max: 1, unit: "unknown" },
39+
distance: { metersPerUnit: null, unit: "unknown" },
40+
elevation: { metersPerUnit: null, min: 0, max: 1, unit: "unknown" },
4041
extent: {
4142
minX: 0,
4243
minY: 0,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "path-profile",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"private": true,
55
"type": "module",
66
"main": "dist-electron/main.cjs",

src/components/path-profile-app.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { getPathProfileApi, hasDesktopBridge } from "~/lib/electron-api";
2828
import { exportProfileStatus } from "~/lib/export-profile-status";
2929
import {
3030
createDefaultLineOfSightEndpoints,
31+
type FresnelZoneUnitScales,
3132
type LineOfSightEndpointId,
3233
type LineOfSightEndpoints,
3334
} from "~/lib/line-of-sight";
@@ -194,6 +195,19 @@ export function PathProfileApp() {
194195
project?.elevation.unit && project.elevation.unit !== "unknown"
195196
? project.elevation.unit
196197
: null;
198+
const fresnelUnitScales = useMemo<FresnelZoneUnitScales | null>(() => {
199+
const horizontalMetersPerUnit = project?.distance.metersPerUnit;
200+
const verticalMetersPerUnit = project?.elevation.metersPerUnit;
201+
202+
if (
203+
!isPositiveFinite(horizontalMetersPerUnit) ||
204+
!isPositiveFinite(verticalMetersPerUnit)
205+
) {
206+
return null;
207+
}
208+
209+
return { horizontalMetersPerUnit, verticalMetersPerUnit };
210+
}, [project]);
197211

198212
const noticeMessages = useMemo(
199213
() => (project ? warnings : ["Open a DEM from File > Open DEM..."]),
@@ -1022,6 +1036,7 @@ export function PathProfileApp() {
10221036
<div className="min-h-0 flex-1 px-4">
10231037
<ProfileChart
10241038
elevationUnit={elevationUnit}
1039+
fresnelUnitScales={fresnelUnitScales}
10251040
lineOfSightEndpoints={lineOfSightEndpoints}
10261041
lineOfSightDrafts={lineOfSightDrafts}
10271042
points={profilePoints}
@@ -1211,6 +1226,10 @@ function errorMessage(error: unknown): string {
12111226
return error instanceof Error ? error.message : String(error);
12121227
}
12131228

1229+
function isPositiveFinite(value: number | null | undefined): value is number {
1230+
return typeof value === "number" && Number.isFinite(value) && value > 0;
1231+
}
1232+
12141233
function themeLabel(theme: ThemeMode): string {
12151234
switch (theme) {
12161235
case "system":

0 commit comments

Comments
 (0)