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

docs: Viewport elevation control #9313

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion docs/api-reference/core/globe-viewport.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Projects world coordinates to pixel coordinates on screen.

Parameters:

* `coordinates` (number[]) - `[longitude, latitude, altitude]`. `altitude` is in meters and default to `0` if not supplied.
* `coordinates` ([number, number, number]) - `[longitude, latitude, altitude]`. `altitude` is in meters and default to `0` if not supplied.
* `opts` (object)
+ `topLeft` (boolean, optional) - Whether projected coords are top left. Default to `true`.

Expand Down
4 changes: 4 additions & 0 deletions docs/upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Breaking changes:

- `PointLight.attenuation` was previously ignored. To retain old behavior, use the default (`[1, 0, 0]`).

### MapViewState / ViewportOptions

- The `position` property changed from `number[]` to `[number, number, number]` (lng, lat, elevation).

## Upgrading to v9.0

**Before you upgrade: known issues**
Expand Down
9 changes: 6 additions & 3 deletions modules/aggregation-layers/src/contour-layer/contour-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends
getValue: ({positions}: {positions: number[]}, index: number, opts: BinOptions) => {
const viewport = this.state.aggregatorViewport;
// project to common space
const p = viewport.projectPosition(positions);
const p = viewport.projectPosition([positions[0], positions[1], positions[2] || 0]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original motivation for this PR is to improve the types, so I would avoid changing the implementation. Adding type casts or better types is great, but you should avoid anything that will modify the behavior at runtime as there is a risk of introducing bugs

const {cellSizeCommon, cellOriginCommon} = opts;
return [
Math.floor((p[0] - cellOriginCommon[0]) / cellSizeCommon[0]),
Expand Down Expand Up @@ -241,7 +241,10 @@ export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends
let viewport = this.context.viewport;

if (bounds && Number.isFinite(bounds[0][0])) {
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2];
let centroid: [number, number] = [
(bounds[0][0] + bounds[1][0]) / 2,
(bounds[0][1] + bounds[1][1]) / 2
];
const {cellSize, gridOrigin} = this.props;
const {unitsPerMeter} = viewport.getDistanceScales(centroid);
cellSizeCommon[0] = unitsPerMeter[0] * cellSize;
Expand Down Expand Up @@ -272,7 +275,7 @@ export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends
binIdRange = getBinIdRange({
dataBounds: bounds,
getBinId: (p: number[]) => {
const positionCommon = viewport.projectFlat(p);
const positionCommon = viewport.projectFlat([p[0], p[1]]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above, better to do p as NumberArray2

return [
Math.floor((positionCommon[0] - cellOriginCommon[0]) / cellSizeCommon[0]),
Math.floor((positionCommon[1] - cellOriginCommon[1]) / cellSizeCommon[1])
Expand Down
9 changes: 6 additions & 3 deletions modules/aggregation-layers/src/grid-layer/grid-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends
}
const viewport = this.state.aggregatorViewport;
// project to common space
const p = viewport.projectPosition(positions);
const p = viewport.projectPosition([positions[0], positions[1], positions[2] || 0]);
const {cellSizeCommon, cellOriginCommon} = opts;
return [
Math.floor((p[0] - cellOriginCommon[0]) / cellSizeCommon[0]),
Expand Down Expand Up @@ -446,7 +446,10 @@ export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends
let viewport = this.context.viewport;

if (bounds && Number.isFinite(bounds[0][0])) {
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2];
let centroid: [number, number] = [
(bounds[0][0] + bounds[1][0]) / 2,
(bounds[0][1] + bounds[1][1]) / 2
];
const {cellSize} = this.props;
const {unitsPerMeter} = viewport.getDistanceScales(centroid);
cellSizeCommon[0] = unitsPerMeter[0] * cellSize;
Expand Down Expand Up @@ -475,7 +478,7 @@ export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends
binIdRange = getBinIdRange({
dataBounds: bounds,
getBinId: (p: number[]) => {
const positionCommon = viewport.projectFlat(p);
const positionCommon = viewport.projectFlat([p[0], p[1]]);
return [
Math.floor((positionCommon[0] - cellOriginCommon[0]) / cellSizeCommon[0]),
Math.floor((positionCommon[1] - cellOriginCommon[1]) / cellSizeCommon[1])
Expand Down
17 changes: 10 additions & 7 deletions modules/aggregation-layers/src/heatmap-layer/heatmap-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,10 +492,10 @@ export default class HeatmapLayer<
// Unproject all 4 corners of the current screen coordinates into world coordinates (lng/lat)
// Takes care of viewport has non zero bearing/pitch (i.e axis not aligned with world coordiante system)
const viewportCorners = [
viewport.unproject([0, 0]),
viewport.unproject([viewport.width, 0]),
viewport.unproject([0, viewport.height]),
viewport.unproject([viewport.width, viewport.height])
viewport.unproject([0, 0, 0]),
viewport.unproject([viewport.width, 0, 0]),
viewport.unproject([0, viewport.height, 0]),
viewport.unproject([viewport.width, viewport.height, 0])
].map(p => p.map(Math.fround));

// #1: get world bounds for current viewport extends
Expand Down Expand Up @@ -546,7 +546,10 @@ export default class HeatmapLayer<
triPositionBuffer!.write(packVertices(viewportCorners, 3));

const textureBounds = viewportCorners.map(p =>
getTextureCoordinates(viewport.projectPosition(p), normalizedCommonBounds!)
getTextureCoordinates(
viewport.projectPosition([p[0], p[1], p[2] || 0]),
normalizedCommonBounds!
)
);
triTexCoordBuffer!.write(packVertices(textureBounds, 2));
}
Expand Down Expand Up @@ -691,8 +694,8 @@ export default class HeatmapLayer<
_commonToWorldBounds(commonBounds) {
const [xMin, yMin, xMax, yMax] = commonBounds;
const {viewport} = this.context;
const bottomLeftWorld = viewport.unprojectPosition([xMin, yMin]);
const topRightWorld = viewport.unprojectPosition([xMax, yMax]);
const bottomLeftWorld = viewport.unprojectPosition([xMin, yMin, 0]);
const topRightWorld = viewport.unprojectPosition([xMax, yMax, 0]);

return bottomLeftWorld.slice(0, 2).concat(topRightWorld.slice(0, 2));
}
Expand Down
9 changes: 6 additions & 3 deletions modules/aggregation-layers/src/hexagon-layer/hexagon-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export default class HexagonLayer<
}
const viewport = this.state.aggregatorViewport;
// project to common space
const p = viewport.projectPosition(positions);
const p = viewport.projectPosition([positions[0], positions[1], positions[2] || 0]);
const {radiusCommon, hexOriginCommon} = opts;
return pointToHexbin(
[p[0] - hexOriginCommon[0], p[1] - hexOriginCommon[1]],
Expand Down Expand Up @@ -451,7 +451,10 @@ export default class HexagonLayer<
let viewport = this.context.viewport;

if (bounds && Number.isFinite(bounds[0][0])) {
let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2];
let centroid: [number, number] = [
(bounds[0][0] + bounds[1][0]) / 2,
(bounds[0][1] + bounds[1][1]) / 2
];
const {radius} = this.props;
const {unitsPerMeter} = viewport.getDistanceScales(centroid);
radiusCommon = unitsPerMeter[0] * radius;
Expand All @@ -474,7 +477,7 @@ export default class HexagonLayer<
binIdRange = getBinIdRange({
dataBounds: bounds,
getBinId: (p: number[]) => {
const positionCommon = viewport.projectFlat(p);
const positionCommon = viewport.projectFlat([p[0], p[1]]);
positionCommon[0] -= hexOriginCommon[0];
positionCommon[1] -= hexOriginCommon[1];
return pointToHexbin(positionCommon, radiusCommon);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default class ScreenGridLayer<
sources: ['positions'],
getValue: ({positions}: {positions: number[]}, index: number, opts: BinOptions) => {
const viewport = this.context.viewport;
const p = viewport.project(positions);
const p = viewport.project([positions[0], positions[1], positions[2] || 0]);
const cellSizePixels: number = opts.cellSizePixels;
if (p[0] < 0 || p[0] >= viewport.width || p[1] < 0 || p[1] >= viewport.height) {
// Not on screen
Expand Down
8 changes: 4 additions & 4 deletions modules/core/src/lib/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export default abstract class Layer<PropsT extends {} = {}> extends Component<
// Public API for users

/** Projects a point with current view state from the current layer's coordinate system to screen */
project(xyz: number[]): number[] {
project(xyz: [number, number, number]): [number, number, number] {
assert(this.internalState);
const viewport = this.internalState.viewport || this.context.viewport;

Expand All @@ -230,20 +230,20 @@ export default abstract class Layer<PropsT extends {} = {}> extends Component<
coordinateSystem: this.props.coordinateSystem
});
const [x, y, z] = worldToPixels(worldPosition, viewport.pixelProjectionMatrix);
return xyz.length === 2 ? [x, y] : [x, y, z];
return [x, y, z];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider this a breaking API change. Could xyz be typed as [number, number] | [number, number, number]?

Not saying yet that we can't make this API change, but generally we'll tradeoff type safety to preserve the API, and not the other way around.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even that will be breaking change as many people out there are likely passing a number[] and will need to add a cast. I would suggest using a function overload, something like:

function project(xyz: [number, number]): [number, number];
function project(xyz: [number, number, number]): [number, number, number];
function project(xyz: Vector2Or3): Vector2Or3 {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we haven't got around to using them everywhere yet, but we have helper types like NumberArray2 that you could use here

}

/** Unprojects a screen pixel to the current view's default coordinate system
Note: this does not reverse `project`. */
unproject(xy: number[]): number[] {
unproject(xy: [number, number, number]): [number, number, number] {
assert(this.internalState);
const viewport = this.internalState.viewport || this.context.viewport;
return viewport.unproject(xy);
}

/** Projects a point with current view state from the current layer's coordinate system to the world space */
projectPosition(
xyz: number[],
xyz: [number, number, number],
params?: {
/** The viewport to use */
viewport?: Viewport;
Expand Down
4 changes: 2 additions & 2 deletions modules/core/src/lib/picking/pick-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ export function getEmptyPickingInfo({
// Find the viewport that contain the picked pixel
pickedViewport = getViewportFromCoordinates(pickInfo?.pickedViewports || viewports, {x, y});
}
let coordinate: number[] | undefined;
let coordinate: [number, number, number] | undefined;
if (pickedViewport) {
const point = [x - pickedViewport.x, y - pickedViewport.y];
const point: [number, number, number] = [x - pickedViewport.x, y - pickedViewport.y, 0];
if (z !== undefined) {
point[2] = z;
}
Expand Down
12 changes: 6 additions & 6 deletions modules/core/src/lib/view-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ type ViewStateOf<ViewT> = ViewT extends View<infer ViewStateT> ? ViewStateT : ne
type OneOfViews<ViewsT extends ViewOrViews> = ViewsT extends null
? MapView
: ViewsT extends View[]
? ViewsT[number]
: ViewsT;
? ViewsT[number]
: ViewsT;
export type AnyViewStateOf<ViewsT extends ViewOrViews> = ViewStateOf<OneOfViews<ViewsT>>;
export type ViewStateMap<ViewsT extends ViewOrViews> = ViewsT extends null
? MapViewState
: ViewsT extends View
? ViewStateOf<ViewsT>
: {[viewId: string]: AnyViewStateOf<ViewsT>};
? ViewStateOf<ViewsT>
: {[viewId: string]: AnyViewStateOf<ViewsT>};

/** This is a very lose type of all "acceptable" viewState
* It's not good for type hinting but matches what may exist internally
Expand Down Expand Up @@ -195,13 +195,13 @@ export default class ViewManager<ViewsT extends View[]> {
* @param {Object} opts.topLeft=true - Whether origin is top left
* @return {Array|null} - [lng, lat, Z] or [X, Y, Z]
*/
unproject(xyz: number[], opts?: {topLeft?: boolean}): number[] | null {
unproject(xyz: [number, number, number], opts?: {topLeft?: boolean}): number[] | null {
const viewports = this.getViewports();
const pixel = {x: xyz[0], y: xyz[1]};
for (let i = viewports.length - 1; i >= 0; --i) {
const viewport = viewports[i];
if (viewport.containsPixel(pixel)) {
const p = xyz.slice();
const p = xyz.slice() as [number, number, number];
p[0] -= viewport.x;
p[1] -= viewport.y;
return viewport.unproject(p, opts);
Expand Down
6 changes: 3 additions & 3 deletions modules/core/src/shaderlib/project/project-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {CoordinateSystem} from '../../lib/constants';
import type Viewport from '../../viewports/viewport';
import type {NumericArray} from '../../types/types';

const DEFAULT_COORDINATE_ORIGIN = [0, 0, 0];
const DEFAULT_COORDINATE_ORIGIN: [number, number, number] = [0, 0, 0];

// In project.glsl, offset modes calculate z differently from LNG_LAT mode.
// offset modes apply the y adjustment (unitsPerMeter2) when projecting z
Expand Down Expand Up @@ -81,7 +81,7 @@ function normalizeParameters(opts: {

/** Get the common space position from world coordinates in the given coordinate system */
export function getWorldPosition(
position: number[],
position: [number, number, number],
{
viewport,
modelMatrix,
Expand Down Expand Up @@ -134,7 +134,7 @@ export function getWorldPosition(
* a reference coordinate system
*/
export function projectPosition(
position: number[],
position: [number, number, number],
params: {
/** The current viewport */
viewport: Viewport;
Expand Down
6 changes: 3 additions & 3 deletions modules/core/src/transitions/linear-interpolator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const DEFAULT_PROPS = ['longitude', 'latitude', 'zoom', 'bearing', 'pitch'];
const DEFAULT_REQUIRED_PROPS = ['longitude', 'latitude', 'zoom'];

type PropsWithAnchor = {
around?: number[];
around?: [number, number, number];
aroundPosition?: number[];
[key: string]: any;
};
Expand All @@ -21,7 +21,7 @@ type PropsWithAnchor = {
*/
export default class LinearInterpolator extends TransitionInterpolator {
opts: {
around?: number[];
around?: [number, number, number];
makeViewport?: (props: Record<string, any>) => Viewport;
};

Expand All @@ -42,7 +42,7 @@ export default class LinearInterpolator extends TransitionInterpolator {
extract?: string[];
required?: string[];
};
around?: number[];
around?: [number, number, number];
makeViewport?: (props: Record<string, any>) => Viewport;
} = {}
) {
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/viewports/first-person-viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type FirstPersonViewportOptions = {
longitude?: number;
/** Latitude of the camera, in the geospatial case. */
latitude?: number;
/** Meter offsets of the camera from the lng-lat anchor point. Default `[0, 0, 0]`. */
/** Meter offsets of the camera from the lng-lat-elevation anchor point. Default `[0, 0, 0]`. */
position?: [number, number, number];
/** Bearing (heading) of the camera in degrees. Default `0` (north). */
bearing?: number;
Expand Down
30 changes: 15 additions & 15 deletions modules/core/src/viewports/globe-viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export type GlobeViewportOptions = {
/** Camera altitude relative to the viewport height, used to control the FOV. Default `1.5` */
altitude?: number;
/* Meter offsets of the viewport center from lng, lat */
position?: number[];
position?: [number, number, number];
/** Zoom level */
zoom?: number;
/** Use orthographic projection */
Expand Down Expand Up @@ -138,10 +138,10 @@ export default class GlobeViewport extends Viewport {
getBounds(options: {z?: number} = {}): [number, number, number, number] {
const unprojectOption = {targetZ: options.z || 0};

const left = this.unproject([0, this.height / 2], unprojectOption);
const top = this.unproject([this.width / 2, 0], unprojectOption);
const right = this.unproject([this.width, this.height / 2], unprojectOption);
const bottom = this.unproject([this.width / 2, this.height], unprojectOption);
const left = this.unproject([0, this.height / 2, 0], unprojectOption);
const top = this.unproject([this.width / 2, 0, 0], unprojectOption);
const right = this.unproject([this.width, this.height / 2, 0], unprojectOption);
const bottom = this.unproject([this.width / 2, this.height, 0], unprojectOption);

if (right[0] < this.longitude) right[0] += 360;
if (left[0] > this.longitude) left[0] -= 360;
Expand All @@ -155,9 +155,9 @@ export default class GlobeViewport extends Viewport {
}

unproject(
xyz: number[],
xyz: [number, number, number],
{topLeft = true, targetZ}: {topLeft?: boolean; targetZ?: number} = {}
): number[] {
): [number, number, number] {
const [x, y, z] = xyz;

const y2 = topLeft ? y : this.height - y;
Expand Down Expand Up @@ -190,10 +190,10 @@ export default class GlobeViewport extends Viewport {
if (Number.isFinite(z)) {
return [X, Y, Z];
}
return Number.isFinite(targetZ) ? [X, Y, targetZ as number] : [X, Y];
return Number.isFinite(targetZ) ? [X, Y, targetZ as number] : [X, Y, 0];
}

projectPosition(xyz: number[]): [number, number, number] {
projectPosition(xyz: [number, number, number]): [number, number, number] {
const [lng, lat, Z = 0] = xyz;
const lambda = lng * DEGREES_TO_RADIANS;
const phi = lat * DEGREES_TO_RADIANS;
Expand All @@ -203,7 +203,7 @@ export default class GlobeViewport extends Viewport {
return [Math.sin(lambda) * cosPhi * D, -Math.cos(lambda) * cosPhi * D, Math.sin(phi) * D];
}

unprojectPosition(xyz: number[]): [number, number, number] {
unprojectPosition(xyz: [number, number, number]): [number, number, number] {
const [x, y, z] = xyz;
const D = vec3.len(xyz);
const phi = Math.asin(z / D);
Expand All @@ -215,15 +215,15 @@ export default class GlobeViewport extends Viewport {
return [lng, lat, Z];
}

projectFlat(xyz: number[]): [number, number] {
return xyz as [number, number];
projectFlat(xy: [number, number]): [number, number] {
return xy;
}

unprojectFlat(xyz: number[]): [number, number] {
return xyz as [number, number];
unprojectFlat(xy: [number, number]): [number, number] {
return xy;
}

panByPosition(coords: number[], pixel: number[]): GlobeViewportOptions {
panByPosition(coords: number[], pixel: [number, number, number]): GlobeViewportOptions {
const fromPosition = this.unproject(pixel);
return {
longitude: coords[0] - fromPosition[0] + this.longitude,
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/viewports/orbit-viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export default class OrbitViewport extends Viewport {
return [X, Y, Z];
}

panByPosition(coords: number[], pixel: number[]): OrbitViewportOptions {
panByPosition(coords: [number, number, number], pixel: number[]): OrbitViewportOptions {
const p0 = this.project(coords);
const nextCenter = [
this.width / 2 + p0[0] - pixel[0],
Expand Down
Loading