Skip to content

Commit

Permalink
Fixed #2703. turf-angle was not doing what was described in the docs
Browse files Browse the repository at this point in the history
Reviewed the implementations of azimuthToBearing, radiansToDegrees, and degreesToRadians to be easier to follow. Small discrepancies in the angle() geojson test fixtures caused by the bearing be taken from the wrong end of each line needed to be fixed. These tests included additional illustrative info meaning they were overly fragile though, so retired those in favour of vanilla unit tests which should be sufficient for a simple function like angle. (#2714)
  • Loading branch information
smallsaucepan authored Sep 17, 2024
1 parent 6889237 commit 2907628
Show file tree
Hide file tree
Showing 14 changed files with 216 additions and 1,407 deletions.
18 changes: 11 additions & 7 deletions packages/turf-angle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,23 @@ function angle(
const B = endPoint;

// Main
const azimuthAO = bearingToAzimuth(
options.mercator !== true ? bearing(A, O) : rhumbBearing(A, O)
const azimuthOA = bearingToAzimuth(
options.mercator !== true ? bearing(O, A) : rhumbBearing(O, A)
);
const azimuthBO = bearingToAzimuth(
options.mercator !== true ? bearing(B, O) : rhumbBearing(B, O)
let azimuthOB = bearingToAzimuth(
options.mercator !== true ? bearing(O, B) : rhumbBearing(O, B)
);
const angleAO = Math.abs(azimuthAO - azimuthBO);
// If OB "trails" OA advance OB one revolution so we get the clockwise angle.
if (azimuthOB < azimuthOA) {
azimuthOB = azimuthOB + 360;
}
const angleAOB = azimuthOB - azimuthOA;

// Explementary angle
if (options.explementary === true) {
return 360 - angleAO;
return 360 - angleAOB;
}
return angleAO;
return angleAOB;
}

export { angle };
Expand Down
277 changes: 189 additions & 88 deletions packages/turf-angle/test.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,186 @@
import test from "tape";
import path from "path";
import { fileURLToPath } from "url";
import { glob } from "glob";
import { loadJsonFileSync } from "load-json-file";
import { writeJsonFileSync } from "write-json-file";
import { sector } from "@turf/sector";
import { bearing } from "@turf/bearing";
import { truncate } from "@turf/truncate";
import { distance } from "@turf/distance";
import { point, round, lineString, featureCollection } from "@turf/helpers";
import { point, round } from "@turf/helpers";
import { angle } from "./index.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

test("turf-angle", (t) => {
glob
.sync(path.join(__dirname, "test", "in", "*.json"))
.forEach((filepath) => {
// Input
const { name } = path.parse(filepath);
const geojson = loadJsonFileSync(filepath);
const [start, mid, end] = geojson.features;

// Results
const angleProperties = {
interiorAngle: round(angle(start, mid, end), 6),
interiorMercatorAngle: round(
angle(start, mid, end, { mercator: true }),
6
),
explementary: false,
fill: "#F00",
stroke: "#F00",
"fill-opacity": 0.3,
};
const angleExplementaryProperties = {
explementaryAngle: round(
angle(start, mid, end, { explementary: true }),
6
),
explementaryMercatorAngle: round(
angle(start, mid, end, { explementary: true, mercator: true }),
6
),
test("turf-angle -- across 0 bearing", (t) => {
t.equal(round(angle([-1, 1], [0, 0], [1, 1])), 90, "90 degrees");

t.end();
});

test("turf-angle -- 90 degrees", (t) => {
t.equal(
round(angle([124, -17], [124, -22], [131, -22]), 6),
91.312527,
"91.312527 degrees"
);
t.equal(
round(angle([124, -17], [124, -22], [131, -22], { explementary: true }), 6),
268.687473,
"268.687473 degrees explementary"
);
t.equal(
round(angle([124, -17], [124, -22], [131, -22], { mercator: true }), 6),
90,
"90 degrees mercator"
);
t.equal(
round(
angle([124, -17], [124, -22], [131, -22], {
explementary: true,
mercator: true,
}),
6
),
270,
"270 degrees explementary mercator"
);
t.end();
});

test("turf-angle -- 180 degrees", (t) => {
t.equal(round(angle([3, -1], [2, 0], [1, 1]), 6), 180, "180 degrees");

t.end();
});

test("turf-angle -- obtuse", (t) => {
t.equal(
round(angle([48.5, 5.5], [51.5, 12], [59, 15.5]), 6),
218.715175,
"218.715175 degrees"
);
t.equal(
round(
angle([48.5, 5.5], [51.5, 12], [59, 15.5], { explementary: true }),
6
),
141.284825,
"141.284825 degrees explementary"
);
t.equal(
round(angle([48.5, 5.5], [51.5, 12], [59, 15.5], { mercator: true }), 6),
219.826106,
"219.826106 degrees mercator"
);
t.equal(
round(
angle([48.5, 5.5], [51.5, 12], [59, 15.5], {
explementary: true,
mercator: true,
}),
6
),
140.173894,
"140.173894 degrees explementary mercator"
);
t.end();
});

test("turf-angle -- obtuse bigger", (t) => {
t.equal(
round(angle([48.5, 5.5], [51.5, 12], [46.5, 19]), 6),
121.330117,
"121.330117 degrees"
);
t.equal(
round(
angle([48.5, 5.5], [51.5, 12], [46.5, 19], { explementary: true }),
6
),
238.669883,
"238.669883 degrees explementary"
);
t.equal(
round(angle([48.5, 5.5], [51.5, 12], [46.5, 19], { mercator: true }), 6),
120.970546,
"120.970546"
);
t.equal(
round(
angle([48.5, 5.5], [51.5, 12], [46.5, 19], {
explementary: true,
mercator: true,
}),
6
),
239.029454,
"239.029454 degrees explementary mercator"
);
t.end();
});

test("turf-angle -- acute", (t) => {
t.equal(
round(angle([48.5, 5.5], [51.5, 12], [44.5, 10.5]), 6),
53.608314,
"53.608314 degrees"
);
t.equal(
round(
angle([48.5, 5.5], [51.5, 12], [44.5, 10.5], { explementary: true }),
6
),
306.391686,
"306.391686 degrees explementary"
);
t.equal(
round(angle([48.5, 5.5], [51.5, 12], [44.5, 10.5], { mercator: true }), 6),
53.166357,
"53.166357 degrees mercator"
);
t.equal(
round(
angle([48.5, 5.5], [51.5, 12], [44.5, 10.5], {
explementary: true,
fill: "#00F",
stroke: "#00F",
"fill-opacity": 0.3,
};
const results = featureCollection([
truncate(
sector(
mid,
distance(mid, start) / 3,
bearing(mid, start),
bearing(mid, end),
{ properties: angleProperties }
)
),
truncate(
sector(
mid,
distance(mid, start) / 2,
bearing(mid, end),
bearing(mid, start),
{ properties: angleExplementaryProperties }
)
),
lineString(
[
start.geometry.coordinates,
mid.geometry.coordinates,
end.geometry.coordinates,
],
{ "stroke-width": 4, stroke: "#222" }
),
start,
mid,
end,
]);

// Save results
const expected = filepath.replace(
path.join("test", "in"),
path.join("test", "out")
);
if (process.env.REGEN) writeJsonFileSync(expected, results);
t.deepEqual(results, loadJsonFileSync(expected), name);
});
mercator: true,
}),
6
),
306.833643,
"306.833643 degrees explementary mercator"
);
t.end();
});

test("turf-angle -- acute inverse", (t) => {
t.equal(
round(angle([44.5, 10.5], [51.5, 12], [48.5, 5.5]), 6),
306.391686,
"306.391686 degrees"
);
t.equal(
round(
angle([44.5, 10.5], [51.5, 12], [48.5, 5.5], { explementary: true }),
6
),
53.608314,
"53.608314 degrees explementary"
);
t.equal(
round(angle([44.5, 10.5], [51.5, 12], [48.5, 5.5], { mercator: true }), 6),
306.833643,
"306.833643 degrees mercator"
);
t.equal(
round(
angle([44.5, 10.5], [51.5, 12], [48.5, 5.5], {
explementary: true,
mercator: true,
}),
6
),
53.166357,
"53.166357 degrees explementary mercator"
);
t.end();
});

test("turf-angle -- simple", (t) => {
t.equal(round(angle([5, 5], [5, 6], [3, 4])), 45, "45 degrees");
t.equal(round(angle([3, 4], [5, 6], [5, 5])), 45, "45 degrees -- inverse");
t.equal(round(angle([3, 4], [5, 6], [5, 5])), 315, "315 degrees -- inverse");
t.equal(
round(angle([3, 4], [5, 6], [5, 5], { explementary: true })),
round(angle([5, 5], [5, 6], [3, 4], { explementary: true })),
360 - 45,
"explementary angle"
);
Expand Down Expand Up @@ -139,3 +224,19 @@ test("turf-angle -- throws", (t) => {

t.end();
});

test("turf-angle -- 2703", (t) => {
const start = [0, 1];
const mid = [0, 0];
const end = [1, 0];
const a = angle(start, mid, end);
t.equal(a, 90, "90 clockwise");

const start2 = [0, 1];
const mid2 = [0, 0];
const end2 = [-1, 0];
const a2 = angle(start2, mid2, end2);
t.equal(a2, 270, "270 clockwise");

t.end();
});
38 changes: 0 additions & 38 deletions packages/turf-angle/test/in/90-degrees.json

This file was deleted.

Loading

0 comments on commit 2907628

Please sign in to comment.