@mui/x-charts-premium 9.8.0 → 9.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BarChartPremium/BarChartPremium.d.mts +1 -1
- package/BarChartPremium/BarChartPremium.d.ts +1 -1
- package/BarChartPremium/BarChartPremium.js +14 -3
- package/BarChartPremium/BarChartPremium.mjs +14 -3
- package/BarChartPremium/RangeBar/AnimatedRangeBarElement.d.mts +2 -1
- package/BarChartPremium/RangeBar/AnimatedRangeBarElement.d.ts +2 -1
- package/BarChartPremium/RangeBar/FocusedRangeBar.d.mts +1 -1
- package/BarChartPremium/RangeBar/FocusedRangeBar.d.ts +1 -1
- package/BarChartPremium/RangeBar/seriesConfig/getSeriesWithDefaultValues.d.mts +11 -11
- package/BarChartPremium/RangeBar/seriesConfig/getSeriesWithDefaultValues.d.ts +11 -11
- package/BarChartPremium/RangeBar/seriesConfig/index.js +2 -2
- package/BarChartPremium/RangeBar/seriesConfig/index.mjs +2 -2
- package/BarChartPremium/RangeBar/seriesConfig/tooltipPosition.d.mts +2 -3
- package/BarChartPremium/RangeBar/seriesConfig/tooltipPosition.d.ts +2 -3
- package/BarChartPremium/RangeBar/seriesConfig/tooltipPosition.js +12 -15
- package/BarChartPremium/RangeBar/seriesConfig/tooltipPosition.mjs +11 -14
- package/BarChartPremium/useBarChartPremiumProps.d.mts +3 -3
- package/BarChartPremium/useBarChartPremiumProps.d.ts +3 -3
- package/CHANGELOG.md +230 -0
- package/CandlestickChart/CandlestickPlot.d.mts +2 -1
- package/CandlestickChart/CandlestickPlot.d.ts +2 -1
- package/CandlestickChart/seriesConfig/OHLCTooltipContent.d.mts +2 -1
- package/CandlestickChart/seriesConfig/OHLCTooltipContent.d.ts +2 -1
- package/CandlestickChart/seriesConfig/getSeriesWithDefaultValues.d.mts +11 -11
- package/CandlestickChart/seriesConfig/getSeriesWithDefaultValues.d.ts +11 -11
- package/CandlestickChart/seriesConfig/index.js +2 -2
- package/CandlestickChart/seriesConfig/index.mjs +2 -2
- package/CandlestickChart/seriesConfig/tooltipPosition.d.mts +2 -3
- package/CandlestickChart/seriesConfig/tooltipPosition.d.ts +2 -3
- package/CandlestickChart/seriesConfig/tooltipPosition.js +10 -14
- package/CandlestickChart/seriesConfig/tooltipPosition.mjs +10 -14
- package/ChartsDataProviderPremium/ChartsDataProviderPremium.d.mts +1 -1
- package/ChartsDataProviderPremium/ChartsDataProviderPremium.d.ts +1 -1
- package/ChartsDataProviderPremium/ChartsDataProviderPremium.js +2 -2
- package/ChartsDataProviderPremium/ChartsDataProviderPremium.mjs +2 -2
- package/ChartsDataProviderPremium/useChartsDataProviderPremiumProps.d.mts +1 -1
- package/ChartsDataProviderPremium/useChartsDataProviderPremiumProps.d.ts +1 -1
- package/ChartsGeoDataProviderPremium/ChartsGeoDataProviderPremium.d.mts +1 -1
- package/ChartsGeoDataProviderPremium/ChartsGeoDataProviderPremium.d.ts +1 -1
- package/ChartsGeoDataProviderPremium/ChartsGeoDataProviderPremium.js +2 -2
- package/ChartsGeoDataProviderPremium/ChartsGeoDataProviderPremium.mjs +2 -2
- package/ChartsGeoDataProviderPremium/ChartsGeoDataProviderPremium.plugins.d.mts +4 -3
- package/ChartsGeoDataProviderPremium/ChartsGeoDataProviderPremium.plugins.d.ts +4 -3
- package/ChartsGeoDataProviderPremium/ChartsGeoDataProviderPremium.plugins.js +2 -1
- package/ChartsGeoDataProviderPremium/ChartsGeoDataProviderPremium.plugins.mjs +2 -1
- package/ChartsGeoDataProviderPremium/index.d.mts +2 -1
- package/ChartsGeoDataProviderPremium/index.d.ts +2 -1
- package/ChartsGeoDataProviderPremium/useChartsGeoDataProviderPremiumProps.d.mts +1 -1
- package/ChartsGeoDataProviderPremium/useChartsGeoDataProviderPremiumProps.d.ts +1 -1
- package/ChartsRadialDataProviderPremium/ChartsRadialDataProviderPremium.d.mts +2 -1
- package/ChartsRadialDataProviderPremium/ChartsRadialDataProviderPremium.d.ts +2 -1
- package/ChartsRadialDataProviderPremium/ChartsRadialDataProviderPremium.js +2 -2
- package/ChartsRadialDataProviderPremium/ChartsRadialDataProviderPremium.mjs +2 -2
- package/ChartsRadialDataProviderPremium/useChartsRadialDataProviderPremiumProps.d.mts +2 -3
- package/ChartsRadialDataProviderPremium/useChartsRadialDataProviderPremiumProps.d.ts +2 -3
- package/ChartsRenderer/components/PaletteOption.d.mts +1 -1
- package/ChartsRenderer/components/PaletteOption.d.ts +1 -1
- package/ChartsZoomSlider/internals/previews/CandlestickPreviewPlot.d.mts +2 -1
- package/ChartsZoomSlider/internals/previews/CandlestickPreviewPlot.d.ts +2 -1
- package/ChartsZoomSlider/internals/previews/RangeBarPreviewPlot.d.mts +1 -1
- package/ChartsZoomSlider/internals/previews/RangeBarPreviewPlot.d.ts +1 -1
- package/HeatmapPremium/HeatmapPlotPremium.d.mts +1 -1
- package/HeatmapPremium/HeatmapPlotPremium.d.ts +1 -1
- package/HeatmapPremium/useHeatmapPremiumProps.d.mts +2 -2
- package/HeatmapPremium/useHeatmapPremiumProps.d.ts +2 -2
- package/Map/FocusedMapShape.d.mts +2 -1
- package/Map/FocusedMapShape.d.ts +2 -1
- package/Map/GeoDataPlot.d.mts +1 -1
- package/Map/GeoDataPlot.d.ts +1 -1
- package/Map/Graticule.d.mts +1 -1
- package/Map/Graticule.d.ts +1 -1
- package/Map/MapImagePlot.d.mts +1 -1
- package/Map/MapImagePlot.d.ts +1 -1
- package/Map/MapShape.d.mts +1 -1
- package/Map/MapShape.d.ts +1 -1
- package/Map/MapShapePlot.d.mts +1 -1
- package/Map/MapShapePlot.d.ts +1 -1
- package/Map/index.d.mts +1 -3
- package/Map/index.d.ts +1 -3
- package/Map/seriesConfig/highlight.d.mts +2 -2
- package/Map/seriesConfig/highlight.d.ts +2 -2
- package/Map/seriesConfig/tooltipPosition.d.mts +1 -1
- package/Map/seriesConfig/tooltipPosition.d.ts +1 -1
- package/Map/seriesConfig/tooltipPosition.js +2 -8
- package/Map/seriesConfig/tooltipPosition.mjs +3 -9
- package/RadialBarChart/RadialBarElement.d.mts +1 -1
- package/RadialBarChart/RadialBarElement.d.ts +1 -1
- package/RadialBarChart/radialBarClasses.d.mts +1 -1
- package/RadialBarChart/radialBarClasses.d.ts +1 -1
- package/RadialBarChart/seriesConfig/index.js +2 -2
- package/RadialBarChart/seriesConfig/index.mjs +2 -2
- package/RadialBarChart/seriesConfig/tooltipPosition.d.mts +2 -3
- package/RadialBarChart/seriesConfig/tooltipPosition.d.ts +2 -3
- package/RadialBarChart/seriesConfig/tooltipPosition.js +4 -6
- package/RadialBarChart/seriesConfig/tooltipPosition.mjs +2 -5
- package/RadialLineChart/RadialArea.d.mts +2 -1
- package/RadialLineChart/RadialArea.d.ts +2 -1
- package/RadialLineChart/RadialAreaPlot.d.mts +1 -1
- package/RadialLineChart/RadialAreaPlot.d.ts +1 -1
- package/RadialLineChart/RadialLine.d.mts +1 -1
- package/RadialLineChart/RadialLine.d.ts +1 -1
- package/RadialLineChart/RadialLineHighlightElement.d.mts +1 -1
- package/RadialLineChart/RadialLineHighlightElement.d.ts +1 -1
- package/RadialLineChart/RadialLineHighlightPlot.d.mts +1 -1
- package/RadialLineChart/RadialLineHighlightPlot.d.ts +1 -1
- package/RadialLineChart/RadialLinePlot.d.mts +1 -1
- package/RadialLineChart/RadialLinePlot.d.ts +1 -1
- package/RadialLineChart/RadialMarkPlot.d.mts +1 -1
- package/RadialLineChart/RadialMarkPlot.d.ts +1 -1
- package/RadialLineChart/radialLineClasses.d.mts +1 -1
- package/RadialLineChart/radialLineClasses.d.ts +1 -1
- package/RadialLineChart/seriesConfig/index.js +2 -2
- package/RadialLineChart/seriesConfig/index.mjs +2 -2
- package/RadialLineChart/seriesConfig/tooltipPosition.d.mts +2 -3
- package/RadialLineChart/seriesConfig/tooltipPosition.d.ts +2 -3
- package/RadialLineChart/seriesConfig/tooltipPosition.js +4 -6
- package/RadialLineChart/seriesConfig/tooltipPosition.mjs +2 -5
- package/ScatterChartPremium/ScatterChartPremium.d.mts +2 -1
- package/ScatterChartPremium/ScatterChartPremium.d.ts +2 -1
- package/ScatterChartPremium/ScatterPlotPremium.d.mts +2 -1
- package/ScatterChartPremium/ScatterPlotPremium.d.ts +2 -1
- package/ScatterChartPremium/webgl/ScatterWebGLPlot.d.mts +2 -1
- package/ScatterChartPremium/webgl/ScatterWebGLPlot.d.ts +2 -1
- package/colorPalettes/complementary/candlestick.d.mts +2 -2
- package/colorPalettes/complementary/candlestick.d.ts +2 -2
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/internals/plugins/allPlugins.d.mts +2 -2
- package/internals/plugins/allPlugins.d.ts +2 -2
- package/internals/plugins/useGeoProjection/factories.d.mts +3 -0
- package/internals/plugins/useGeoProjection/factories.d.ts +3 -0
- package/internals/plugins/useGeoProjection/factories.js +29 -0
- package/internals/plugins/useGeoProjection/factories.mjs +23 -0
- package/internals/plugins/useGeoProjection/projection.utils.d.mts +17 -0
- package/internals/plugins/useGeoProjection/projection.utils.d.ts +17 -0
- package/internals/plugins/useGeoProjection/projection.utils.js +67 -0
- package/internals/plugins/useGeoProjection/projection.utils.mjs +59 -0
- package/internals/plugins/useGeoProjection/useGeoProjection.js +7 -40
- package/internals/plugins/useGeoProjection/useGeoProjection.mjs +7 -40
- package/internals/plugins/useGeoProjection/useGeoProjection.selectors.d.mts +47 -11
- package/internals/plugins/useGeoProjection/useGeoProjection.selectors.d.ts +47 -11
- package/internals/plugins/useGeoProjection/useGeoProjection.selectors.js +118 -8
- package/internals/plugins/useGeoProjection/useGeoProjection.selectors.mjs +117 -8
- package/internals/plugins/useGeoProjection/useGeoProjection.types.d.mts +63 -2
- package/internals/plugins/useGeoProjection/useGeoProjection.types.d.ts +63 -2
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/usePanOnDrag.d.mts +11 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/usePanOnDrag.d.ts +11 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/usePanOnDrag.js +65 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/usePanOnDrag.mjs +58 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/useZoomOnPinch.d.mts +13 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/useZoomOnPinch.d.ts +13 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/useZoomOnPinch.js +62 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/useZoomOnPinch.mjs +56 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/useZoomOnWheel.d.mts +13 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/useZoomOnWheel.d.ts +13 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/useZoomOnWheel.js +64 -0
- package/internals/plugins/useGeoProjectionZoom/gestureHooks/useZoomOnWheel.mjs +58 -0
- package/internals/plugins/useGeoProjectionZoom/index.d.mts +2 -0
- package/internals/plugins/useGeoProjectionZoom/index.d.ts +2 -0
- package/internals/plugins/useGeoProjectionZoom/index.js +12 -0
- package/internals/plugins/useGeoProjectionZoom/index.mjs +1 -0
- package/internals/plugins/useGeoProjectionZoom/mapZoom.utils.d.mts +31 -0
- package/internals/plugins/useGeoProjectionZoom/mapZoom.utils.d.ts +31 -0
- package/internals/plugins/useGeoProjectionZoom/mapZoom.utils.js +187 -0
- package/internals/plugins/useGeoProjectionZoom/mapZoom.utils.mjs +177 -0
- package/internals/plugins/useGeoProjectionZoom/rotationMath.d.mts +23 -0
- package/internals/plugins/useGeoProjectionZoom/rotationMath.d.ts +23 -0
- package/internals/plugins/useGeoProjectionZoom/rotationMath.js +190 -0
- package/internals/plugins/useGeoProjectionZoom/rotationMath.mjs +182 -0
- package/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.d.mts +3 -0
- package/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.d.ts +3 -0
- package/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.js +174 -0
- package/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.mjs +166 -0
- package/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.types.d.mts +152 -0
- package/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.types.d.ts +152 -0
- package/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.types.js +5 -0
- package/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.types.mjs +1 -0
- package/package.json +6 -6
- package/plugins/selectors/useChartCandlestickPosition.selectors.d.mts +1 -1
- package/plugins/selectors/useChartCandlestickPosition.selectors.d.ts +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.WHEEL_ZOOM_STEP = exports.BUTTON_ZOOM_STEP = void 0;
|
|
7
|
+
exports.clampTranslationAxis = clampTranslationAxis;
|
|
8
|
+
exports.getDefaultMapInteraction = getDefaultMapInteraction;
|
|
9
|
+
exports.getProjectionFamily = getProjectionFamily;
|
|
10
|
+
exports.getRotation = getRotation;
|
|
11
|
+
exports.getTranslation = getTranslation;
|
|
12
|
+
var _d3Geo = require("@mui/x-charts-vendor/d3-geo");
|
|
13
|
+
var _internals = require("@mui/x-charts/internals");
|
|
14
|
+
var _useGeoProjection = require("../useGeoProjection/useGeoProjection.selectors");
|
|
15
|
+
var _createGetVisibleCoordinate = require("../../createGetVisibleCoordinate");
|
|
16
|
+
var _rotationMath = require("./rotationMath");
|
|
17
|
+
/** Multiplicative zoom step applied per wheel tick. */
|
|
18
|
+
const WHEEL_ZOOM_STEP = exports.WHEEL_ZOOM_STEP = 1.1;
|
|
19
|
+
/** Multiplicative zoom step applied per `zoomIn`/`zoomOut` call. */
|
|
20
|
+
const BUTTON_ZOOM_STEP = exports.BUTTON_ZOOM_STEP = 1.3;
|
|
21
|
+
const PROJECTION_FAMILY = {
|
|
22
|
+
azimuthalEqualArea: 'azimuthal',
|
|
23
|
+
azimuthalEquidistant: 'azimuthal',
|
|
24
|
+
gnomonic: 'azimuthal',
|
|
25
|
+
orthographic: 'azimuthal',
|
|
26
|
+
stereographic: 'azimuthal',
|
|
27
|
+
conicConformal: 'conic',
|
|
28
|
+
conicEqualArea: 'conic',
|
|
29
|
+
conicEquidistant: 'conic',
|
|
30
|
+
albers: 'conic',
|
|
31
|
+
albersUsa: 'albersUsa',
|
|
32
|
+
equirectangular: 'cylindrical',
|
|
33
|
+
mercator: 'cylindrical',
|
|
34
|
+
transverseMercator: 'cylindrical',
|
|
35
|
+
equalEarth: 'cylindrical',
|
|
36
|
+
naturalEarth1: 'cylindrical'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The interaction that feels natural for each projection family, used as the default for
|
|
41
|
+
* `rotationAllowed`/`translationAllowed` when the consumer does not set them explicitly.
|
|
42
|
+
*/
|
|
43
|
+
const FAMILY_INTERACTION = {
|
|
44
|
+
azimuthal: {
|
|
45
|
+
rotationAllowed: 'both',
|
|
46
|
+
translationAllowed: 'none'
|
|
47
|
+
},
|
|
48
|
+
cylindrical: {
|
|
49
|
+
rotationAllowed: 'longitude',
|
|
50
|
+
translationAllowed: 'y'
|
|
51
|
+
},
|
|
52
|
+
conic: {
|
|
53
|
+
rotationAllowed: 'none',
|
|
54
|
+
translationAllowed: 'both'
|
|
55
|
+
},
|
|
56
|
+
albersUsa: {
|
|
57
|
+
rotationAllowed: 'none',
|
|
58
|
+
translationAllowed: 'both'
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function getProjectionFamily(projection) {
|
|
62
|
+
if (projection == null) {
|
|
63
|
+
return 'cylindrical'; // fallback to avoid useless edge cases
|
|
64
|
+
}
|
|
65
|
+
if (typeof projection === 'string') {
|
|
66
|
+
return PROJECTION_FAMILY[projection] ?? 'cylindrical';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Try to guess if users provided custom projection
|
|
70
|
+
|
|
71
|
+
// Conic projections expose the `parallels` accessor (e.g. conicConformal, albers).
|
|
72
|
+
if ('parallels' in projection) {
|
|
73
|
+
return 'conic';
|
|
74
|
+
}
|
|
75
|
+
// Composite projections such as `albersUsa` stitch several conics together and so drop `rotate`
|
|
76
|
+
// (there is no single sphere to spin). Treat them like the conic family they are built from.
|
|
77
|
+
if (typeof projection.rotate !== 'function') {
|
|
78
|
+
return 'conic';
|
|
79
|
+
}
|
|
80
|
+
// Azimuthal projections clip the sphere to a disc, so their clip angle sits strictly between 0 and
|
|
81
|
+
// 180° (e.g. 90° for orthographic). Flat projections leave it at 0° (no clipping).
|
|
82
|
+
const clipAngle = projection.clipAngle?.();
|
|
83
|
+
if (typeof clipAngle === 'number' && clipAngle > 0 && clipAngle < 180) {
|
|
84
|
+
return 'azimuthal';
|
|
85
|
+
}
|
|
86
|
+
return 'cylindrical';
|
|
87
|
+
}
|
|
88
|
+
function getDefaultMapInteraction(projection) {
|
|
89
|
+
return FAMILY_INTERACTION[getProjectionFamily(projection)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* For a given projection, apply a zoom factor to the scale and return the rotation that renders
|
|
94
|
+
* `geoPoint` at the `to` pixel coordinates, as `[longitude, latitude, roll]` (the view `center` plus
|
|
95
|
+
* the third d3 rotation angle).
|
|
96
|
+
*
|
|
97
|
+
* - With `rotationAllowed: 'both+roll'`, the roll is free to move but kept as stable as possible: the
|
|
98
|
+
* shortest-arc rotation dragging `geoPoint` onto `to` is composed with the current orientation, so
|
|
99
|
+
* the roll only drifts as much as strictly required.
|
|
100
|
+
* - With `rotationAllowed: 'both'`, both longitude and latitude are free but the roll is kept fixed at
|
|
101
|
+
* the current value.
|
|
102
|
+
* - With `rotationAllowed: 'longitude'`, only the longitude is solved; the latitude and roll are kept fixed.
|
|
103
|
+
* - With `rotationAllowed: 'none'`, `null` is returned so the caller keeps the current rotation.
|
|
104
|
+
*/
|
|
105
|
+
function getRotation(projection, geoPoint, to, zoomFactor = 1, rotationAllowed = 'both') {
|
|
106
|
+
if (!projection.invert || !projection.rotate) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const rotate = projection.rotate?.();
|
|
110
|
+
const scale = projection.scale();
|
|
111
|
+
projection.scale(scale * zoomFactor);
|
|
112
|
+
projection.rotate?.([0, 0]);
|
|
113
|
+
const q = (0, _createGetVisibleCoordinate.createGetVisibleCoordinate)(projection)(to);
|
|
114
|
+
|
|
115
|
+
// Reset projection modifications to avoid side effects.
|
|
116
|
+
projection.rotate?.(rotate);
|
|
117
|
+
projection.scale(scale);
|
|
118
|
+
if (!geoPoint || !q) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
switch (rotationAllowed) {
|
|
122
|
+
case 'both+roll':
|
|
123
|
+
return (0, _rotationMath.getMinimalRollRotation)(rotate, geoPoint, q);
|
|
124
|
+
case 'both':
|
|
125
|
+
return (0, _rotationMath.getNoRollRotation)(rotate, geoPoint, q);
|
|
126
|
+
case 'longitude':
|
|
127
|
+
return (0, _rotationMath.getLongitudeRotation)(rotate, geoPoint, q);
|
|
128
|
+
case 'none':
|
|
129
|
+
default:
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function clampTranslationAxis(value, init, boundingBox0, boundingBox1, areaStart, areaEnd, gap) {
|
|
134
|
+
// Get min/mas translation allowed
|
|
135
|
+
const max = areaStart - boundingBox0 + init + gap;
|
|
136
|
+
const min = areaEnd - boundingBox1 + init - gap;
|
|
137
|
+
if (min > max) {
|
|
138
|
+
// If not translation accept the movement only if it reduces the gap
|
|
139
|
+
const prevGap0 = boundingBox0 - areaStart;
|
|
140
|
+
const prevGap1 = areaEnd - boundingBox1;
|
|
141
|
+
const gap = value - init;
|
|
142
|
+
const nextGap0 = boundingBox0 + gap - areaStart;
|
|
143
|
+
const nextGap1 = areaEnd - (boundingBox1 + gap);
|
|
144
|
+
if (Math.max(prevGap0, prevGap1) > Math.max(nextGap0, nextGap1)) {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
return init;
|
|
148
|
+
}
|
|
149
|
+
return Math.min(max, Math.max(min, value));
|
|
150
|
+
}
|
|
151
|
+
function getTranslation(store, projection, geoPoint, to, translationAllowed = 'both', maxEmptySpace = 0, currentTranslation = [0, 0]) {
|
|
152
|
+
if (!projection.invert || translationAllowed === 'none') {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const q = (0, _createGetVisibleCoordinate.createGetVisibleCoordinate)(projection)(to);
|
|
156
|
+
if (!geoPoint || !q) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const allowX = translationAllowed === 'both' || translationAllowed === 'x';
|
|
160
|
+
const allowY = translationAllowed === 'both' || translationAllowed === 'y';
|
|
161
|
+
const projectedGeoPoint = projection(geoPoint);
|
|
162
|
+
const projectedQ = projection(q);
|
|
163
|
+
if (!projectedGeoPoint || !projectedQ) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const deltaX = projectedQ[0] - projectedGeoPoint[0];
|
|
167
|
+
const deltaY = projectedQ[1] - projectedGeoPoint[1];
|
|
168
|
+
const initTranslation = projection.translate();
|
|
169
|
+
const drawingArea = (0, _internals.selectorChartDrawingArea)(store.state);
|
|
170
|
+
let tx = currentTranslation[0];
|
|
171
|
+
let ty = currentTranslation[1];
|
|
172
|
+
|
|
173
|
+
// Clamp translation
|
|
174
|
+
const geoData = Number.isFinite(maxEmptySpace) ? (0, _useGeoProjection.selectorChartGeoData)(store.state) : null;
|
|
175
|
+
if (geoData) {
|
|
176
|
+
const [[bx0, by0], [bx1, by1]] = (0, _d3Geo.geoPath)(projection).bounds(geoData);
|
|
177
|
+
if (allowX) {
|
|
178
|
+
const translationX = clampTranslationAxis(initTranslation[0] + deltaX, initTranslation[0], bx0, bx1, drawingArea.left, drawingArea.left + drawingArea.width, maxEmptySpace * drawingArea.width);
|
|
179
|
+
tx = (translationX - drawingArea.left - drawingArea.width / 2) / drawingArea.width;
|
|
180
|
+
}
|
|
181
|
+
if (allowY) {
|
|
182
|
+
const translationY = clampTranslationAxis(initTranslation[1] + deltaY, initTranslation[1], by0, by1, drawingArea.top, drawingArea.top + drawingArea.height, maxEmptySpace * drawingArea.height);
|
|
183
|
+
ty = (translationY - drawingArea.top - drawingArea.height / 2) / drawingArea.height;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return [tx, ty];
|
|
187
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { geoPath } from '@mui/x-charts-vendor/d3-geo';
|
|
2
|
+
import { selectorChartDrawingArea } from '@mui/x-charts/internals';
|
|
3
|
+
import { selectorChartGeoData } from "../useGeoProjection/useGeoProjection.selectors.mjs";
|
|
4
|
+
import { createGetVisibleCoordinate } from "../../createGetVisibleCoordinate.mjs";
|
|
5
|
+
import { getLongitudeRotation, getMinimalRollRotation, getNoRollRotation } from "./rotationMath.mjs";
|
|
6
|
+
|
|
7
|
+
/** Multiplicative zoom step applied per wheel tick. */
|
|
8
|
+
export const WHEEL_ZOOM_STEP = 1.1;
|
|
9
|
+
/** Multiplicative zoom step applied per `zoomIn`/`zoomOut` call. */
|
|
10
|
+
export const BUTTON_ZOOM_STEP = 1.3;
|
|
11
|
+
const PROJECTION_FAMILY = {
|
|
12
|
+
azimuthalEqualArea: 'azimuthal',
|
|
13
|
+
azimuthalEquidistant: 'azimuthal',
|
|
14
|
+
gnomonic: 'azimuthal',
|
|
15
|
+
orthographic: 'azimuthal',
|
|
16
|
+
stereographic: 'azimuthal',
|
|
17
|
+
conicConformal: 'conic',
|
|
18
|
+
conicEqualArea: 'conic',
|
|
19
|
+
conicEquidistant: 'conic',
|
|
20
|
+
albers: 'conic',
|
|
21
|
+
albersUsa: 'albersUsa',
|
|
22
|
+
equirectangular: 'cylindrical',
|
|
23
|
+
mercator: 'cylindrical',
|
|
24
|
+
transverseMercator: 'cylindrical',
|
|
25
|
+
equalEarth: 'cylindrical',
|
|
26
|
+
naturalEarth1: 'cylindrical'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The interaction that feels natural for each projection family, used as the default for
|
|
31
|
+
* `rotationAllowed`/`translationAllowed` when the consumer does not set them explicitly.
|
|
32
|
+
*/
|
|
33
|
+
const FAMILY_INTERACTION = {
|
|
34
|
+
azimuthal: {
|
|
35
|
+
rotationAllowed: 'both',
|
|
36
|
+
translationAllowed: 'none'
|
|
37
|
+
},
|
|
38
|
+
cylindrical: {
|
|
39
|
+
rotationAllowed: 'longitude',
|
|
40
|
+
translationAllowed: 'y'
|
|
41
|
+
},
|
|
42
|
+
conic: {
|
|
43
|
+
rotationAllowed: 'none',
|
|
44
|
+
translationAllowed: 'both'
|
|
45
|
+
},
|
|
46
|
+
albersUsa: {
|
|
47
|
+
rotationAllowed: 'none',
|
|
48
|
+
translationAllowed: 'both'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export function getProjectionFamily(projection) {
|
|
52
|
+
if (projection == null) {
|
|
53
|
+
return 'cylindrical'; // fallback to avoid useless edge cases
|
|
54
|
+
}
|
|
55
|
+
if (typeof projection === 'string') {
|
|
56
|
+
return PROJECTION_FAMILY[projection] ?? 'cylindrical';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Try to guess if users provided custom projection
|
|
60
|
+
|
|
61
|
+
// Conic projections expose the `parallels` accessor (e.g. conicConformal, albers).
|
|
62
|
+
if ('parallels' in projection) {
|
|
63
|
+
return 'conic';
|
|
64
|
+
}
|
|
65
|
+
// Composite projections such as `albersUsa` stitch several conics together and so drop `rotate`
|
|
66
|
+
// (there is no single sphere to spin). Treat them like the conic family they are built from.
|
|
67
|
+
if (typeof projection.rotate !== 'function') {
|
|
68
|
+
return 'conic';
|
|
69
|
+
}
|
|
70
|
+
// Azimuthal projections clip the sphere to a disc, so their clip angle sits strictly between 0 and
|
|
71
|
+
// 180° (e.g. 90° for orthographic). Flat projections leave it at 0° (no clipping).
|
|
72
|
+
const clipAngle = projection.clipAngle?.();
|
|
73
|
+
if (typeof clipAngle === 'number' && clipAngle > 0 && clipAngle < 180) {
|
|
74
|
+
return 'azimuthal';
|
|
75
|
+
}
|
|
76
|
+
return 'cylindrical';
|
|
77
|
+
}
|
|
78
|
+
export function getDefaultMapInteraction(projection) {
|
|
79
|
+
return FAMILY_INTERACTION[getProjectionFamily(projection)];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* For a given projection, apply a zoom factor to the scale and return the rotation that renders
|
|
84
|
+
* `geoPoint` at the `to` pixel coordinates, as `[longitude, latitude, roll]` (the view `center` plus
|
|
85
|
+
* the third d3 rotation angle).
|
|
86
|
+
*
|
|
87
|
+
* - With `rotationAllowed: 'both+roll'`, the roll is free to move but kept as stable as possible: the
|
|
88
|
+
* shortest-arc rotation dragging `geoPoint` onto `to` is composed with the current orientation, so
|
|
89
|
+
* the roll only drifts as much as strictly required.
|
|
90
|
+
* - With `rotationAllowed: 'both'`, both longitude and latitude are free but the roll is kept fixed at
|
|
91
|
+
* the current value.
|
|
92
|
+
* - With `rotationAllowed: 'longitude'`, only the longitude is solved; the latitude and roll are kept fixed.
|
|
93
|
+
* - With `rotationAllowed: 'none'`, `null` is returned so the caller keeps the current rotation.
|
|
94
|
+
*/
|
|
95
|
+
export function getRotation(projection, geoPoint, to, zoomFactor = 1, rotationAllowed = 'both') {
|
|
96
|
+
if (!projection.invert || !projection.rotate) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const rotate = projection.rotate?.();
|
|
100
|
+
const scale = projection.scale();
|
|
101
|
+
projection.scale(scale * zoomFactor);
|
|
102
|
+
projection.rotate?.([0, 0]);
|
|
103
|
+
const q = createGetVisibleCoordinate(projection)(to);
|
|
104
|
+
|
|
105
|
+
// Reset projection modifications to avoid side effects.
|
|
106
|
+
projection.rotate?.(rotate);
|
|
107
|
+
projection.scale(scale);
|
|
108
|
+
if (!geoPoint || !q) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
switch (rotationAllowed) {
|
|
112
|
+
case 'both+roll':
|
|
113
|
+
return getMinimalRollRotation(rotate, geoPoint, q);
|
|
114
|
+
case 'both':
|
|
115
|
+
return getNoRollRotation(rotate, geoPoint, q);
|
|
116
|
+
case 'longitude':
|
|
117
|
+
return getLongitudeRotation(rotate, geoPoint, q);
|
|
118
|
+
case 'none':
|
|
119
|
+
default:
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function clampTranslationAxis(value, init, boundingBox0, boundingBox1, areaStart, areaEnd, gap) {
|
|
124
|
+
// Get min/mas translation allowed
|
|
125
|
+
const max = areaStart - boundingBox0 + init + gap;
|
|
126
|
+
const min = areaEnd - boundingBox1 + init - gap;
|
|
127
|
+
if (min > max) {
|
|
128
|
+
// If not translation accept the movement only if it reduces the gap
|
|
129
|
+
const prevGap0 = boundingBox0 - areaStart;
|
|
130
|
+
const prevGap1 = areaEnd - boundingBox1;
|
|
131
|
+
const gap = value - init;
|
|
132
|
+
const nextGap0 = boundingBox0 + gap - areaStart;
|
|
133
|
+
const nextGap1 = areaEnd - (boundingBox1 + gap);
|
|
134
|
+
if (Math.max(prevGap0, prevGap1) > Math.max(nextGap0, nextGap1)) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
return init;
|
|
138
|
+
}
|
|
139
|
+
return Math.min(max, Math.max(min, value));
|
|
140
|
+
}
|
|
141
|
+
export function getTranslation(store, projection, geoPoint, to, translationAllowed = 'both', maxEmptySpace = 0, currentTranslation = [0, 0]) {
|
|
142
|
+
if (!projection.invert || translationAllowed === 'none') {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const q = createGetVisibleCoordinate(projection)(to);
|
|
146
|
+
if (!geoPoint || !q) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const allowX = translationAllowed === 'both' || translationAllowed === 'x';
|
|
150
|
+
const allowY = translationAllowed === 'both' || translationAllowed === 'y';
|
|
151
|
+
const projectedGeoPoint = projection(geoPoint);
|
|
152
|
+
const projectedQ = projection(q);
|
|
153
|
+
if (!projectedGeoPoint || !projectedQ) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const deltaX = projectedQ[0] - projectedGeoPoint[0];
|
|
157
|
+
const deltaY = projectedQ[1] - projectedGeoPoint[1];
|
|
158
|
+
const initTranslation = projection.translate();
|
|
159
|
+
const drawingArea = selectorChartDrawingArea(store.state);
|
|
160
|
+
let tx = currentTranslation[0];
|
|
161
|
+
let ty = currentTranslation[1];
|
|
162
|
+
|
|
163
|
+
// Clamp translation
|
|
164
|
+
const geoData = Number.isFinite(maxEmptySpace) ? selectorChartGeoData(store.state) : null;
|
|
165
|
+
if (geoData) {
|
|
166
|
+
const [[bx0, by0], [bx1, by1]] = geoPath(projection).bounds(geoData);
|
|
167
|
+
if (allowX) {
|
|
168
|
+
const translationX = clampTranslationAxis(initTranslation[0] + deltaX, initTranslation[0], bx0, bx1, drawingArea.left, drawingArea.left + drawingArea.width, maxEmptySpace * drawingArea.width);
|
|
169
|
+
tx = (translationX - drawingArea.left - drawingArea.width / 2) / drawingArea.width;
|
|
170
|
+
}
|
|
171
|
+
if (allowY) {
|
|
172
|
+
const translationY = clampTranslationAxis(initTranslation[1] + deltaY, initTranslation[1], by0, by1, drawingArea.top, drawingArea.top + drawingArea.height, maxEmptySpace * drawingArea.height);
|
|
173
|
+
ty = (translationY - drawingArea.top - drawingArea.height / 2) / drawingArea.height;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return [tx, ty];
|
|
177
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the new
|
|
3
|
+
* `[longitude, latitude, roll]` (the view `center` plus `roll`) that drags `geoPoint` onto the
|
|
4
|
+
* identity-frame screen coordinate `q` using the shortest-arc rotation composed with the current
|
|
5
|
+
* orientation. Because the delta is the minimal rotation moving the grabbed point to the target, the
|
|
6
|
+
* roll only changes as much as strictly required.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getMinimalRollRotation(rotate: [number, number, number], geoPoint: [number, number], targetGeoPoint: [number, number]): [number, number, number];
|
|
9
|
+
/**
|
|
10
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the
|
|
11
|
+
* `[longitude, latitude, roll]` that drags `geoPoint` onto the identity-frame screen coordinate
|
|
12
|
+
* `targetGeoPoint` while keeping the roll (`gamma`) unchanged: only the longitude and latitude move.
|
|
13
|
+
* When the target is out of reach for a roll-preserving rotation, the closest reachable orientation
|
|
14
|
+
* is returned.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getNoRollRotation(rotate: [number, number, number], geoPoint: [number, number], targetGeoPoint: [number, number]): [number, number, number];
|
|
17
|
+
/**
|
|
18
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the
|
|
19
|
+
* `[longitude, latitude, roll]` that pans `geoPoint` toward the identity-frame screen coordinate
|
|
20
|
+
* `targetGeoPoint` along the longitude axis only: the latitude and roll are left untouched and just
|
|
21
|
+
* the longitude is solved so `geoPoint` reaches the target's longitude.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getLongitudeRotation(rotate: [number, number, number], geoPoint: [number, number], targetGeoPoint: [number, number]): [number, number, number];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the new
|
|
3
|
+
* `[longitude, latitude, roll]` (the view `center` plus `roll`) that drags `geoPoint` onto the
|
|
4
|
+
* identity-frame screen coordinate `q` using the shortest-arc rotation composed with the current
|
|
5
|
+
* orientation. Because the delta is the minimal rotation moving the grabbed point to the target, the
|
|
6
|
+
* roll only changes as much as strictly required.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getMinimalRollRotation(rotate: [number, number, number], geoPoint: [number, number], targetGeoPoint: [number, number]): [number, number, number];
|
|
9
|
+
/**
|
|
10
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the
|
|
11
|
+
* `[longitude, latitude, roll]` that drags `geoPoint` onto the identity-frame screen coordinate
|
|
12
|
+
* `targetGeoPoint` while keeping the roll (`gamma`) unchanged: only the longitude and latitude move.
|
|
13
|
+
* When the target is out of reach for a roll-preserving rotation, the closest reachable orientation
|
|
14
|
+
* is returned.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getNoRollRotation(rotate: [number, number, number], geoPoint: [number, number], targetGeoPoint: [number, number]): [number, number, number];
|
|
17
|
+
/**
|
|
18
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the
|
|
19
|
+
* `[longitude, latitude, roll]` that pans `geoPoint` toward the identity-frame screen coordinate
|
|
20
|
+
* `targetGeoPoint` along the longitude axis only: the latitude and roll are left untouched and just
|
|
21
|
+
* the longitude is solved so `geoPoint` reaches the target's longitude.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getLongitudeRotation(rotate: [number, number, number], geoPoint: [number, number], targetGeoPoint: [number, number]): [number, number, number];
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getLongitudeRotation = getLongitudeRotation;
|
|
7
|
+
exports.getMinimalRollRotation = getMinimalRollRotation;
|
|
8
|
+
exports.getNoRollRotation = getNoRollRotation;
|
|
9
|
+
const DEG = Math.PI / 180;
|
|
10
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Rotate a `[longitude, latitude]` coordinate around the projection's center axis (the x-axis
|
|
14
|
+
* pointing to `[0, 0]`) by `gamma` degrees.
|
|
15
|
+
*
|
|
16
|
+
* This is the roll component of a d3 rotation `[lambda, phi, gamma]`, isolated so it can be undone
|
|
17
|
+
* on the target coordinate before solving the longitude/latitude rotation. A d3 rotation composes as
|
|
18
|
+
* `rollGamma ∘ geoRotation([lambda, phi])`, so pre-applying `rollGamma(-gamma)` to the target keeps
|
|
19
|
+
* the roll fixed while the yaw/pitch solver stays a two-angle problem.
|
|
20
|
+
*/
|
|
21
|
+
function rollCoordinate(coord, gamma) {
|
|
22
|
+
if (!gamma) {
|
|
23
|
+
return coord;
|
|
24
|
+
}
|
|
25
|
+
const g = gamma * DEG;
|
|
26
|
+
const cosG = Math.cos(g);
|
|
27
|
+
const sinG = Math.sin(g);
|
|
28
|
+
const lon = coord[0] * DEG;
|
|
29
|
+
const lat = coord[1] * DEG;
|
|
30
|
+
const x = Math.cos(lon) * Math.cos(lat);
|
|
31
|
+
const y = Math.sin(lon) * Math.cos(lat);
|
|
32
|
+
const z = Math.sin(lat);
|
|
33
|
+
// Rotation about the x-axis by `g`.
|
|
34
|
+
const yRolled = y * cosG - z * sinG;
|
|
35
|
+
const zRolled = y * sinG + z * cosG;
|
|
36
|
+
return [Math.atan2(yRolled, x) / DEG, Math.asin(clamp(zRolled, -1, 1)) / DEG];
|
|
37
|
+
}
|
|
38
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Unit vector on the sphere for a `[longitude, latitude]` coordinate (degrees).
|
|
42
|
+
*/
|
|
43
|
+
function unitVector([lonDeg, latDeg]) {
|
|
44
|
+
const lon = lonDeg * DEG;
|
|
45
|
+
const lat = latDeg * DEG;
|
|
46
|
+
const cosLat = Math.cos(lat);
|
|
47
|
+
return [Math.cos(lon) * cosLat, Math.sin(lon) * cosLat, Math.sin(lat)];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rotate vector `v` around the unit axis `k` by `angle` radians (Rodrigues' rotation formula).
|
|
52
|
+
*/
|
|
53
|
+
function rotateAroundAxis(k, angle, v) {
|
|
54
|
+
const cos = Math.cos(angle);
|
|
55
|
+
const sin = Math.sin(angle);
|
|
56
|
+
const dot = (k[0] * v[0] + k[1] * v[1] + k[2] * v[2]) * (1 - cos);
|
|
57
|
+
const kv = cross(k, v);
|
|
58
|
+
return [v[0] * cos + kv[0] * sin + k[0] * dot, v[1] * cos + kv[1] * sin + k[1] * dot, v[2] * cos + kv[2] * sin + k[2] * dot];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the new
|
|
63
|
+
* `[longitude, latitude, roll]` (the view `center` plus `roll`) that drags `geoPoint` onto the
|
|
64
|
+
* identity-frame screen coordinate `q` using the shortest-arc rotation composed with the current
|
|
65
|
+
* orientation. Because the delta is the minimal rotation moving the grabbed point to the target, the
|
|
66
|
+
* roll only changes as much as strictly required.
|
|
67
|
+
*/
|
|
68
|
+
function getMinimalRollRotation(rotate, geoPoint, targetGeoPoint) {
|
|
69
|
+
const l = rotate[0] * DEG;
|
|
70
|
+
const p = rotate[1] * DEG;
|
|
71
|
+
const g = (rotate[2] ?? 0) * DEG;
|
|
72
|
+
const cl = Math.cos(l);
|
|
73
|
+
const sl = Math.sin(l);
|
|
74
|
+
const cp = Math.cos(p);
|
|
75
|
+
const sp = Math.sin(p);
|
|
76
|
+
const cg = Math.cos(g);
|
|
77
|
+
const sg = Math.sin(g);
|
|
78
|
+
|
|
79
|
+
// Columns of the current rotation matrix `M = Rx(g)·Ry(-p)·Rz(l)`, which is exactly
|
|
80
|
+
// d3's `geoRotation([lambda, phi, gamma])` expressed on cartesian unit vectors.
|
|
81
|
+
const col0 = [cp * cl, cg * sl - sg * sp * cl, sg * sl + cg * sp * cl];
|
|
82
|
+
const col1 = [-cp * sl, cg * cl + sg * sp * sl, sg * cl - cg * sp * sl];
|
|
83
|
+
const col2 = [-sp, -sg * cp, cg * cp];
|
|
84
|
+
const point = unitVector(geoPoint);
|
|
85
|
+
// Where the grabbed point currently sits in the identity (screen) frame: `M · point`.
|
|
86
|
+
const current = [col0[0] * point[0] + col1[0] * point[1] + col2[0] * point[2], col0[1] * point[0] + col1[1] * point[1] + col2[1] * point[2], col0[2] * point[0] + col1[2] * point[1] + col2[2] * point[2]];
|
|
87
|
+
const target = unitVector(targetGeoPoint);
|
|
88
|
+
|
|
89
|
+
// Shortest-arc rotation taking `current` onto `target`, applied to `M` column by column
|
|
90
|
+
// (`M_new = delta · M`).
|
|
91
|
+
const axis = cross(current, target);
|
|
92
|
+
const s = Math.hypot(axis[0], axis[1], axis[2]);
|
|
93
|
+
let n0 = col0;
|
|
94
|
+
let n1 = col1;
|
|
95
|
+
let n2 = col2;
|
|
96
|
+
if (s > 1e-12) {
|
|
97
|
+
const dot = clamp(current[0] * target[0] + current[1] * target[1] + current[2] * target[2], -1, 1);
|
|
98
|
+
const angle = Math.atan2(s, dot);
|
|
99
|
+
const k = [axis[0] / s, axis[1] / s, axis[2] / s];
|
|
100
|
+
n0 = rotateAroundAxis(k, angle, col0);
|
|
101
|
+
n1 = rotateAroundAxis(k, angle, col1);
|
|
102
|
+
n2 = rotateAroundAxis(k, angle, col2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Decompose `M_new = Rx(g')·Ry(-p')·Rz(l')` back into d3 angles.
|
|
106
|
+
const phi = Math.asin(clamp(-n2[0], -1, 1));
|
|
107
|
+
const lambda = Math.atan2(-n1[0], n0[0]);
|
|
108
|
+
const gamma = Math.atan2(-n2[1], n2[2]);
|
|
109
|
+
|
|
110
|
+
// `rotate = [-center[0], -center[1], roll]`, so invert the sign for the view center.
|
|
111
|
+
return [-lambda / DEG, -phi / DEG, gamma / DEG];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the
|
|
116
|
+
* `[longitude, latitude, roll]` that drags `geoPoint` onto the identity-frame screen coordinate
|
|
117
|
+
* `targetGeoPoint` while keeping the roll (`gamma`) unchanged: only the longitude and latitude move.
|
|
118
|
+
* When the target is out of reach for a roll-preserving rotation, the closest reachable orientation
|
|
119
|
+
* is returned.
|
|
120
|
+
*/
|
|
121
|
+
function getNoRollRotation(rotate, geoPoint, targetGeoPoint) {
|
|
122
|
+
const roll = rotate[2] ?? 0;
|
|
123
|
+
|
|
124
|
+
// A d3 rotation composes as `rollGamma ∘ geoRotation([lambda, phi])`, so undo the roll on the
|
|
125
|
+
// target: what remains is a plain (roll-free) `Ry(-phi)·Rz(lambda)` mapping `geoPoint` onto it.
|
|
126
|
+
const [px, py, pz] = unitVector(geoPoint);
|
|
127
|
+
const [tx, ty, tz] = unitVector(rollCoordinate(targetGeoPoint, -roll));
|
|
128
|
+
const lambdaOld = rotate[0] * DEG;
|
|
129
|
+
const phiOld = rotate[1] * DEG;
|
|
130
|
+
|
|
131
|
+
// `Rz(lambda)` spins `geoPoint` on the circle of radius `r` in the xy-plane, keeping its
|
|
132
|
+
// z-component; `Ry(-phi)` then tilts it. So after `Rz` the y-component (`uy`) must already equal
|
|
133
|
+
// the target's, and the xy-radius bounds how far the latitude can reach.
|
|
134
|
+
const r = Math.hypot(px, py);
|
|
135
|
+
if (r < 1e-12) {
|
|
136
|
+
// `geoPoint` sits on a pole where `Rz(lambda)` is a no-op: keep the current longitude and only
|
|
137
|
+
// solve the tilt.
|
|
138
|
+
const phi = Math.atan2(tz, tx) - Math.atan2(pz, 0);
|
|
139
|
+
return [-lambdaOld / DEG, -phi / DEG, roll];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// `clamp` keeps the nearest reachable point when the target latitude is out of range (`|ty| > r`).
|
|
143
|
+
const uy = clamp(ty, -r, r);
|
|
144
|
+
const uxMagnitude = Math.sqrt(Math.max(0, r * r - uy * uy));
|
|
145
|
+
const solveFor = ux => ({
|
|
146
|
+
// `Rz(lambda)` rotates `(px, py)` onto `(ux, uy)`.
|
|
147
|
+
lambda: Math.atan2(uy, ux) - Math.atan2(py, px),
|
|
148
|
+
// `Ry(-phi)` rotates `(ux, pz)` onto `(tx, tz)`.
|
|
149
|
+
phi: Math.atan2(tz, tx) - Math.atan2(pz, ux)
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Two branches (`±ux`) solve the equation; pick the one closest to the current orientation so the
|
|
153
|
+
// rotation stays continuous while dragging.
|
|
154
|
+
const wrap = angle => Math.atan2(Math.sin(angle), Math.cos(angle));
|
|
155
|
+
const distanceToCurrent = ({
|
|
156
|
+
lambda,
|
|
157
|
+
phi
|
|
158
|
+
}) => Math.abs(wrap(lambda - lambdaOld)) + Math.abs(wrap(phi - phiOld));
|
|
159
|
+
const positive = solveFor(uxMagnitude);
|
|
160
|
+
const negative = solveFor(-uxMagnitude);
|
|
161
|
+
const best = distanceToCurrent(positive) <= distanceToCurrent(negative) ? positive : negative;
|
|
162
|
+
return [-best.lambda / DEG, -best.phi / DEG, roll];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Given the current d3 `rotate` `[lambda, phi, gamma]` (degrees), returns the
|
|
167
|
+
* `[longitude, latitude, roll]` that pans `geoPoint` toward the identity-frame screen coordinate
|
|
168
|
+
* `targetGeoPoint` along the longitude axis only: the latitude and roll are left untouched and just
|
|
169
|
+
* the longitude is solved so `geoPoint` reaches the target's longitude.
|
|
170
|
+
*/
|
|
171
|
+
function getLongitudeRotation(rotate, geoPoint, targetGeoPoint) {
|
|
172
|
+
const roll = rotate[2] ?? 0;
|
|
173
|
+
const latitude = -rotate[1]; // the fixed center latitude (degrees)
|
|
174
|
+
|
|
175
|
+
// Undo the roll on the target, then keep the pitch locked to the current latitude and solve the
|
|
176
|
+
// yaw so `geoPoint` lands on the target's longitude.
|
|
177
|
+
const [tx, ty, tz] = unitVector(rollCoordinate(targetGeoPoint, -roll));
|
|
178
|
+
const lonG = geoPoint[0] * DEG;
|
|
179
|
+
const c1 = latitude * DEG;
|
|
180
|
+
const c0 = lonG - Math.atan2(ty, tx * Math.cos(c1) - tz * Math.sin(c1));
|
|
181
|
+
|
|
182
|
+
// Normalize the longitude into `[-180, 180]`.
|
|
183
|
+
let longitude = c0 / DEG % 360;
|
|
184
|
+
if (longitude > 180) {
|
|
185
|
+
longitude -= 360;
|
|
186
|
+
} else if (longitude < -180) {
|
|
187
|
+
longitude += 360;
|
|
188
|
+
}
|
|
189
|
+
return [longitude, latitude, roll];
|
|
190
|
+
}
|