Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed turf-angle to consistently measure angles clockwise #2714

Merged
merged 1 commit into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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