Is there a neat way to map the output of Math.atan2
to another range while keeping the result still in [-180°, 180°], as if the calculation does not start from the positive X-axis (rightwards = 0°), but from any given angle?
This question and this question have a similar gist, but it only concerned a particular solution.
IMO, the method of Math.atan2
is fairly quirky, because it does not align with cartographical convention to calculate azimuth angles, which should start from positive Y-axis (or upwards) instead. However, tweaking the mathematics behind arctan
isn't obviously a good choice; I believe that mapping the output angle should be better.
My finding is, you need to classify the angles into at least 2 categories in order to ensure the result to be in the range of [-180°, 180°]. I know that rotate()
can handle results out of this range and no rounding is needed. However, I was asking a way to let the output stay in that range.
For instance, an output of a 30-degree-clockwise shifted Math.atan2 should look like this:
Before (Right=0°) | After (30°SE=0°) |
---|---|
0° | -30° |
30° | 0° |
60° | 30° |
120° | 90° |
180° | 150° |
-180° | 150° |
-120° | -150° |
-60° | -90° |
-30° | -60° |
I noted that there is a discontinuity at -180°/180° in this mapping method, which caused most of the problem here. A small two-way conditional is probably sufficient to solve this problem, but is there any simple way to achieve this result (of any given rotate angle θ
) without using any conditionals?
A visual depiction. Any coordinates that fall into the purple overlapping area need a different way to calculate.
Here is a short code I once used to solve this problem.
Context: the code's intention was to get a rough latitude & longitude coordinate after the user clicks inside a circle representing Earth. The guide map on it was rotatable, but to change the orientation of the map image would affect too much other stuff, so I wanted to find a simple way to process the output, regardless of in which direction the map was placed on the Earth panel. I have removed some irrelevant code here.
earth.addEventListener('click', function (e) {
// The coordinates of the point of click within the circle is retrieved as (clickX, clickY).
// If the final angle is positive, offset it with 180°. Otherwise offset it with -180°. Notice that it is a specific case.
let longitude = Math.atan2(clickY, clickX) * 180 / Math.PI;
if (longitude <= 0) {
longitude = (-180 - longitude);
} else {
longitude = (180 - longitude);
}
console.log("Longitude: " + longitude);
}
If the offset was 30°, and we try to achieve the effect of the table above, the code should be written instead like:
if (longitude <= -150 && longitude > -180) {
longitude = (longitude - 30);
} else {
longitude = (longitude + 330);
}
Obviously, if we don't need the result to be within [-180°, 180°], the if-else was unnecessary at all. But for this case, are there any better ways?
I found a "double modulo" that helps on the issue Y.T. and Ture points out
const tbody = document.getElementById('table-body');
const offsetField = document.getElementById('offset');
const testAngles = [0, 30, 60, 120, 180, -180, -120, -60, -30];
const shiftedAtan2Deg = (y, x, zeroAxisDeg) => {
const radToDeg = 180 / Math.PI;
const degToRad = Math.PI / 180;
let angle = Math.atan2(y, x) * radToDeg - zeroAxisDeg;
// Normalize to [-180°, 180°] using double-modulo
angle = (((angle + 180) % 360) + 360) % 360 - 180;
return angle;
};
const testTable = (zeroAxis) => {
tbody.innerHTML = '';
if (zeroAxis > 360) zeroAxis = 360;
if (zeroAxis < -360) zeroAxis = -360;
offsetField.value = zeroAxis;
for (const angle of testAngles) {
const rad = angle * Math.PI / 180;
const y = Math.sin(rad);
const x = Math.cos(rad);
const shifted = shiftedAtan2Deg(y, x, zeroAxis);
tbody.innerHTML += `<tr><td>${angle}°</td><td>${shifted.toFixed(1)}°</td></tr>`;
}
};
// generating your table with offset°
offsetField.addEventListener('input', (e) => testTable(+e.target.value));
testTable(30); // initial
table {
border-collapse: collapse;
max-width: 210px;
}
td,
th {
text-align: right;
padding: 4px 8px;
border: 1px solid #ccc;
}
#offset {
width: 40px;
}
<table>
<thead>
<tr>
<th>Input</th>
<th>Zero at <input type="number" id="offset" min="-360" max="360" step="0.1" value="30"/></label>°</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>