基于矩形区域的相机自动定位与飞行控制实现

在地理信息系统 (GIS) 和数字孪生应用中,经常需要实现相机对特定区域的自动定位功能。本文将介绍如何基于矩形区域范围和指定俯仰角,计算相机最佳观测位置并实现平滑飞行,特别适合片区选中后的自动定位场景。

核心功能与参数说明

该功能的核心是通过矩形区域范围和俯仰角参数,自动计算相机应处的三维坐标,并控制相机平滑飞行到目标位置。主要涉及两个关键参数:

  • boundingBox:目标区域的边界范围,可接受两种格式
    • 数组形式:[west, south, east, north](经纬度,度)
    • 对象形式:{west, south, east, north}(经纬度,度)
  • pitch:相机俯仰角(角度),负值表示向下倾斜,如 - 45 表示向下 45° 观测

实现步骤详解

1. 计算垂直观测矩形的初始相机位置

首先需要计算从正上方垂直观测目标矩形时的相机位置,这一功能由rectangleCameraPosition3D函数实现(借鉴 Cesium 源码思想):

javascript 复制代码
function rectangleCameraPosition3D(rectangle) {
    // 处理跨越日界线的情况
    if (rectangle.west > rectangle.east) {
        rectangle.east += TWO_PI;
    }

    // 计算矩形中心经纬度(考虑极点和大地线精度)
    const longitude = (rectangle.west + rectangle.east) * 0.5;
    let latitude;
    
    // 特殊处理近极点区域,否则使用大地线插值计算中心
    if (rectangle.south < -PI_OVER_TWO + RADIANS_PER_DEGREE && 
        rectangle.north > PI_OVER_TWO - RADIANS_PER_DEGREE) {
        latitude = 0.0;
    } else {
        const northCartographic = SSmap.Cartographic.create(longitude, rectangle.north, 0.0);
        const southCartographic = SSmap.Cartographic.create(longitude, rectangle.south, 0.0);
        const geodesic = new GeodesicPath(northCartographic, southCartographic);
        const centerResult = geodesic.interpolate(0.5);
        latitude = centerResult.latitude;
    }

    // 计算矩形中心的笛卡尔坐标
    const centerCartographic = SSmap.Cartographic.create(longitude, latitude, 0.0);
    const ellipsoid = SSmap.Ellipsoid.WGS84();
    const center = ellipsoid.cartographicToCartesian(centerCartographic);

    // 计算矩形各角点相对中心的坐标
    const points = [
        SSmap.Cartographic.create(rectangle.east, rectangle.north, 0), // 东北
        SSmap.Cartographic.create(rectangle.west, rectangle.north, 0), // 西北
        // 更多关键点位...
    ].map(cart => CartesianMath.subtract(ellipsoid.cartographicToCartesian(cart), center));

    // 构建相机局部坐标系(指向地心的方向向量)
    const cameraRF = {
        direction: CartesianMath.minus(ellipsoid.geodeticSurfaceNormal(center)),
        right: CartesianMath.cross(cameraRF.direction, SSmap.Cartesian3.create(0, 0, 1)).normalize(),
        up: CartesianMath.cross(cameraRF.right, cameraRF.direction)
    };

    // 计算确保矩形完全可见的最小距离
    const camera = GlobalViewer.scene.mainCamera;
    const tanPhi = Math.tan(camera.fovy * 0.5 * RADIANS_PER_DEGREE);
    const tanTheta = camera.aspectRatio * tanPhi;
    
    let d = Math.max(
        // 计算所有关键点位所需的观测距离
        computeD(cameraRF.direction, cameraRF.up, points[0], tanPhi),
        computeD(cameraRF.direction, cameraRF.up, points[1], tanPhi),
        // 更多点位计算...
        computeD(cameraRF.direction, cameraRF.right, points[0], tanTheta),
        // 更多点位计算...
    );

    // 返回最终位置:从中心沿相机反方向移动距离d
    return CartesianMath.adding(center, CartesianMath.vectorByNumber(cameraRF.direction, -d));
}

该函数核心逻辑:

  • 精确计算矩形几何中心(考虑地球椭球特性)
  • 构建以中心为原点的相机局部坐标系
  • 计算能完整显示矩形的最小相机距离(基于视场角计算)
  • 确保所有角点都在相机视锥范围内

2. 构建局部坐标系矩阵

以矩形中心点为原点,构建东北天 (ENU) 局部坐标系,实现世界坐标与局部坐标的转换:

javascript 复制代码
// 计算矩形中心点
let center = rectangle.center();
// 应用高度偏移(如果需要)
if (altitude) {
    center.height = altitude;
}
// 转换为笛卡尔坐标
center = center.toCartesian3();

// 构建ENU到世界坐标的转换矩阵
const ENUToWorld = ellipsoid.eastNorthUpToFixedFrame(center);
// 构建世界坐标到ENU的逆矩阵
const worldToENU = ENUToWorld.inverted();

3. 坐标转换与旋转处理

将初始相机位置转换到局部坐标系,进行旋转调整后再转回世界坐标:

javascript 复制代码
// 1. 将垂直观测位置转换到局部坐标系
const localValue = SSmap.Matrix4.multiplyByVector3(worldToENU, position.toVector3());

// 2. 构造旋转四元数:绕X轴旋转(90 + pitch)度
// 垂直向下为-90度,此时旋转0度保持垂直
const rot = SSmap.Quaternion.fromAxisAndAngle(
    SSmap.Vector3.create(1, 0, 0),  // X轴为旋转轴
    90 + pitch                      // 旋转角度计算
);
const rotMatrix = rot.toRotationMatrix();

// 3. 应用旋转得到新的局部坐标
const localValue2 = SSmap.Matrix3.multiplyByVector3(rotMatrix, localValue);

// 4. 转换回世界坐标
const worldValue = SSmap.Matrix4.multiplyByVector3(ENUToWorld, localValue2);

4. 执行飞行操作

最后通过相机控制器实现平滑飞行到目标位置:

javascript 复制代码
function flyToRectChangePitch({
    boundingBox,
    pitch = -90,
    duration = 1,
    altitude = 0,
}) {
    // 计算目标位置
    const position = rectChangePitch({
        boundingBox,
        pitch,
        altitude
    });

    // 执行飞行
    const cameraController = GlobalViewer.scene.mainCamera.cameraController();
    cameraController.flyTo(
        position,
        duration,
        0,  // heading
        pitch,
        0   // roll
    );
}

应用场景:片区自动定位

以实际应用场景为例:

  1. 系统从接口获取几十个片区的边界数据
  2. 在地图上绘制各片区的电子围栏边界
  3. 当用户选中某个片区时,触发自动定位逻辑:
  4. 相机将平滑飞行到最佳观测位置,以 45° 角清晰展示整个片区

这种实现方式确保了无论片区大小和位置如何,都能自动计算出最合适的观测点,极大提升了交互体验和工作效率。

使用示例

javascript 复制代码
// 选中片区后调用
flyToRectChangePitch({
    boundingBox: [113.998592, 22.790307, 114.059972, 22.570420], // 片区边界
    pitch: -45, // 45度俯视角
    duration: 2, // 2秒飞行时间
});

完整代码

javascript 复制代码
/**
 * 模块说明:根据矩形范围和俯仰角计算相机飞行位置
 * 主要功能:
 * 1. 计算垂直观测矩形的最佳相机位置 (rectangleCameraPosition3D)
 * 2. 支持调整俯仰角的相机位置计算 (rectChangePitch)
 * 3. 封装飞行漫游动作 (flyToRectChangePitch)
 *
 * 包含算法:
 * - 笛卡尔向量运算
 * - Vincenty 公式(用于高精度的大地测量距离和路径计算)
 */

const TWO_PI = Math.PI * 2;
const PI_OVER_TWO = Math.PI / 2;
const RADIANS_PER_DEGREE = Math.PI / 180;
const EPSILON12 = 1e-12;

/**
 * 基础笛卡尔坐标运算工具集
 * 提供向量的叉乘、点乘、加减、缩放等基本运算
 */
const CartesianMath = {
    /**
     * 向量叉乘:计算两个向量所在平面的法向量
     */
    cross(left, right) {
        const x = left.y * right.z - left.z * right.y;
        const y = left.z * right.x - left.x * right.z;
        const z = left.x * right.y - left.y * right.x;
        return SSmap.Cartesian3.create(x, y, z);
    },
    /**
     * 向量点乘:计算向量投影或夹角余弦
     */
    dot(left, right) {
        return left.x * right.x + left.y * right.y + left.z * right.z;
    },
    subtract(carA, carB) {
        return SSmap.Cartesian3.create(
            carA.x - carB.x,
            carA.y - carB.y,
            carA.z - carB.z
        );
    },
    adding(carA, carB) {
        return SSmap.Cartesian3.create(
            carA.x + carB.x,
            carA.y + carB.y,
            carA.z + carB.z
        );
    },
    minus(car) {
        return SSmap.Cartesian3.create(-car.x, -car.y, -car.z);
    },
    vectorByNumber(car, num) {
        return SSmap.Cartesian3.create(car.x * num, car.y * num, car.z * num);
    },
};

/**
 * 计算单个点所需的相机后退距离
 * @param {Cartesian3} direction 相机朝向向量
 * @param {Cartesian3} upOrRight 相机上向量或右向量
 * @param {Cartesian3} corner 目标点相对于中心的坐标
 * @param {Number} tanThetaOrPhi 视场角的一半的正切值
 * @returns {Number} 所需距离
 */
function computeD(direction, upOrRight, corner, tanThetaOrPhi) {
    const opposite = Math.abs(CartesianMath.dot(upOrRight, corner));
    // opposite / tan(fov/2) 得到在该FOV下看清opposite高度所需的距离
    // 减去点在视线方向的投影,因为如果点在中心前方,相机需要更靠后
    return opposite / tanThetaOrPhi - CartesianMath.dot(direction, corner);
}

/**
 * 封装 Vincenty 公式的大地测量路径计算类
 * 用于处理两点间的高精度距离计算和路径插值
 */
class GeodesicPath {
    constructor(start, end) {
        this.start = start;
        this.end = end;
        this.start.height = 0;
        this.end.height = 0;

        this.ellipsoid = SSmap.Ellipsoid.WGS84();

        // 计算结果存储
        this.distance = 0;
        this.startHeading = 0;
        this.endHeading = 0;
        this.uSquared = 0;
        this.constants = {
            a: 0,
            b: 0,
            f: 0,
            cosineHeading: 0,
            sineHeading: 0,
            tanU: 0,
            cosineU: 0,
            sineU: 0,
            sigma: 0,
            sineAlpha: 0,
            sineSquaredAlpha: 0,
            cosineSquaredAlpha: 0,
            cosineAlpha: 0,
            u2Over4: 0,
            u4Over16: 0,
            u6Over64: 0,
            u8Over256: 0,
            a0: 0,
            a1: 0,
            a2: 0,
            a3: 0,
            distanceRatio: 0,
        };

        this._calculatePath();
    }

    /**
     * 初始化路径计算:执行 Vincenty 逆解算并预计算插值常量
     */
    _calculatePath() {
        // 1. 执行 Vincenty 逆解算 (Inverse Formula)
        // 计算两点间的大地线距离、起始方位角等
        this._vincentyInverseFormula(
            this.ellipsoid.maximumRadius,
            this.ellipsoid.minimumRadius,
            this.start.longitude,
            this.start.latitude,
            this.end.longitude,
            this.end.latitude
        );

        // 2. 预计算插值所需的常量 (setEndPoints 逻辑)
        const uSquared = this.uSquared;
        const a = this.ellipsoid.maximumRadius;
        const b = this.ellipsoid.minimumRadius;
        const f = (a - b) / a;

        const cosineHeading = Math.cos(this.startHeading);
        const sineHeading = Math.sin(this.startHeading);

        const tanU = (1 - f) * Math.tan(this.start.latitude);
        const cosineU = 1.0 / Math.sqrt(1.0 + tanU * tanU);
        const sineU = cosineU * tanU;

        const sigma = Math.atan2(tanU, cosineHeading);

        const sineAlpha = cosineU * sineHeading;
        const sineSquaredAlpha = sineAlpha * sineAlpha;

        const cosineSquaredAlpha = 1.0 - sineSquaredAlpha;
        const cosineAlpha = Math.sqrt(cosineSquaredAlpha);

        const u2Over4 = uSquared / 4.0;
        const u4Over16 = u2Over4 * u2Over4;
        const u6Over64 = u4Over16 * u2Over4;
        const u8Over256 = u4Over16 * u4Over16;

        // 计算展开系数 A0, A1, A2, A3
        const a0 =
            1.0 +
            u2Over4 -
            (3.0 * u4Over16) / 4.0 +
            (5.0 * u6Over64) / 4.0 -
            (175.0 * u8Over256) / 64.0;
        const a1 =
            1.0 - u2Over4 + (15.0 * u4Over16) / 8.0 - (35.0 * u6Over64) / 8.0;
        const a2 = 1.0 - 3.0 * u2Over4 + (35.0 * u4Over16) / 4.0;
        const a3 = 1.0 - 5.0 * u2Over4;

        const distanceRatio =
            a0 * sigma -
            (a1 * Math.sin(2.0 * sigma) * u2Over4) / 2.0 -
            (a2 * Math.sin(4.0 * sigma) * u4Over16) / 16.0 -
            (a3 * Math.sin(6.0 * sigma) * u6Over64) / 48.0 -
            (Math.sin(8.0 * sigma) * 5.0 * u8Over256) / 512;

        // 保存常量
        Object.assign(this.constants, {
            a,
            b,
            f,
            cosineHeading,
            sineHeading,
            tanU,
            cosineU,
            sineU,
            sigma,
            sineAlpha,
            sineSquaredAlpha,
            cosineSquaredAlpha,
            cosineAlpha,
            u2Over4,
            u4Over16,
            u6Over64,
            u8Over256,
            a0,
            a1,
            a2,
            a3,
            distanceRatio,
        });
    }

    /**
     * Vincenty 逆解算公式
     * 计算椭球面上两点间的距离和方位角
     */
    _vincentyInverseFormula(
        major,
        minor,
        firstLongitude,
        firstLatitude,
        secondLongitude,
        secondLatitude
    ) {
        const eff = (major - minor) / major;
        const l = secondLongitude - firstLongitude;

        const u1 = Math.atan((1 - eff) * Math.tan(firstLatitude));
        const u2 = Math.atan((1 - eff) * Math.tan(secondLatitude));

        const cosineU1 = Math.cos(u1);
        const sineU1 = Math.sin(u1);
        const cosineU2 = Math.cos(u2);
        const sineU2 = Math.sin(u2);

        const cc = cosineU1 * cosineU2;
        const cs = cosineU1 * sineU2;
        const ss = sineU1 * sineU2;
        const sc = sineU1 * cosineU2;

        let lambda = l;
        let lambdaDot = TWO_PI;

        let sigma,
            cosineSigma,
            sineSigma,
            cosineSquaredAlpha,
            cosineTwiceSigmaMidpoint;
        let sineAlpha;

        // 迭代求解 Lambda
        do {
            const cosineLambda = Math.cos(lambda);
            const sineLambda = Math.sin(lambda);

            const temp = cs - sc * cosineLambda;
            sineSigma = Math.sqrt(
                cosineU2 * cosineU2 * sineLambda * sineLambda + temp * temp
            );
            cosineSigma = ss + cc * cosineLambda;

            sigma = Math.atan2(sineSigma, cosineSigma);

            if (sineSigma === 0.0) {
                sineAlpha = 0.0;
                cosineSquaredAlpha = 1.0;
            } else {
                sineAlpha = (cc * sineLambda) / sineSigma;
                cosineSquaredAlpha = 1.0 - sineAlpha * sineAlpha;
            }

            lambdaDot = lambda;

            cosineTwiceSigmaMidpoint =
                cosineSigma - (2.0 * ss) / cosineSquaredAlpha;

            if (!isFinite(cosineTwiceSigmaMidpoint)) {
                cosineTwiceSigmaMidpoint = 0.0;
            }

            lambda =
                l +
                this._computeDeltaLambda(
                    eff,
                    sineAlpha,
                    cosineSquaredAlpha,
                    sigma,
                    sineSigma,
                    cosineSigma,
                    cosineTwiceSigmaMidpoint
                );
        } while (Math.abs(lambda - lambdaDot) > EPSILON12);

        const uSquared =
            (cosineSquaredAlpha * (major * major - minor * minor)) /
            (minor * minor);
        const A =
            1.0 +
            (uSquared *
                (4096.0 +
                    uSquared *
                        (uSquared * (320.0 - 175.0 * uSquared) - 768.0))) /
                16384.0;
        const B =
            (uSquared *
                (256.0 +
                    uSquared * (uSquared * (74.0 - 47.0 * uSquared) - 128.0))) /
            1024.0;

        const cosineSquaredTwiceSigmaMidpoint =
            cosineTwiceSigmaMidpoint * cosineTwiceSigmaMidpoint;
        const deltaSigma =
            B *
            sineSigma *
            (cosineTwiceSigmaMidpoint +
                (B *
                    (cosineSigma *
                        (2.0 * cosineSquaredTwiceSigmaMidpoint - 1.0) -
                        (B *
                            cosineTwiceSigmaMidpoint *
                            (4.0 * sineSigma * sineSigma - 3.0) *
                            (4.0 * cosineSquaredTwiceSigmaMidpoint - 3.0)) /
                            6.0)) /
                    4.0);

        this.distance = minor * A * (sigma - deltaSigma);
        this.startHeading = Math.atan2(
            cosineU2 * Math.sin(lambda),
            cs - sc * Math.cos(lambda)
        );
        this.endHeading = Math.atan2(
            cosineU1 * Math.sin(lambda),
            cs * Math.cos(lambda) - sc
        );
        this.uSquared = uSquared;
    }

    _computeDeltaLambda(
        f,
        sineAlpha,
        cosineSquaredAlpha,
        sigma,
        sineSigma,
        cosineSigma,
        cosineTwiceSigmaMidpoint
    ) {
        const C = this._computeC(f, cosineSquaredAlpha);
        return (
            (1.0 - C) *
            f *
            sineAlpha *
            (sigma +
                C *
                    sineSigma *
                    (cosineTwiceSigmaMidpoint +
                        C *
                            cosineSigma *
                            (2.0 *
                                cosineTwiceSigmaMidpoint *
                                cosineTwiceSigmaMidpoint -
                                1.0)))
        );
    }

    _computeC(f, cosineSquaredAlpha) {
        return (
            (f *
                cosineSquaredAlpha *
                (4.0 + f * (4.0 - 3.0 * cosineSquaredAlpha))) /
            16.0
        );
    }

    /**
     * 根据比例插值计算路径上的点
     * @param {Number} fraction 插值比例 (0.0 - 1.0)
     * @returns {Cartographic} 插值点的经纬度坐标
     */
    interpolate(fraction) {
        const distance = this.distance * fraction;
        const c = this.constants;

        const s = c.distanceRatio + distance / c.b;

        // 计算三角函数值
        const cosine2S = Math.cos(2.0 * s);
        const cosine4S = Math.cos(4.0 * s);
        const cosine6S = Math.cos(6.0 * s);
        const sine2S = Math.sin(2.0 * s);
        const sine4S = Math.sin(4.0 * s);
        const sine6S = Math.sin(6.0 * s);
        const sine8S = Math.sin(8.0 * s);

        const s2 = s * s;
        const s3 = s * s2;

        // 计算 Sigma
        let sigma =
            (2.0 * s3 * c.u8Over256 * cosine2S) / 3.0 +
            s *
                (1.0 -
                    c.u2Over4 +
                    (7.0 * c.u4Over16) / 4.0 -
                    (15.0 * c.u6Over64) / 4.0 +
                    (579.0 * c.u8Over256) / 64.0 -
                    (c.u4Over16 -
                        (15.0 * c.u6Over64) / 4.0 +
                        (187.0 * c.u8Over256) / 16.0) *
                        cosine2S -
                    ((5.0 * c.u6Over64) / 4.0 - (115.0 * c.u8Over256) / 16.0) *
                        cosine4S -
                    (29.0 * c.u8Over256 * cosine6S) / 16.0) +
            (c.u2Over4 / 2.0 -
                c.u4Over16 +
                (71.0 * c.u6Over64) / 32.0 -
                (85.0 * c.u8Over256) / 16.0) *
                sine2S +
            ((5.0 * c.u4Over16) / 16.0 -
                (5.0 * c.u6Over64) / 4.0 +
                (383.0 * c.u8Over256) / 96.0) *
                sine4S -
            s2 *
                ((c.u6Over64 - (11.0 * c.u8Over256) / 2.0) * sine2S +
                    (5.0 * c.u8Over256 * sine4S) / 2.0) +
            ((29.0 * c.u6Over64) / 96.0 - (29.0 * c.u8Over256) / 16.0) *
                sine6S +
            (539.0 * c.u8Over256 * sine8S) / 1536.0;

        const theta = Math.asin(Math.sin(sigma) * c.cosineAlpha);
        const latitude = Math.atan((c.a / c.b) * Math.tan(theta));

        // 重新定义 sigma 为相对于纬度的参数
        sigma = sigma - c.sigma;

        const cosineTwiceSigmaMidpoint = Math.cos(2.0 * c.sigma + sigma);
        const sineSigma = Math.sin(sigma);
        const cosineSigma = Math.cos(sigma);

        const cc = c.cosineU * cosineSigma;
        const ss = c.sineU * sineSigma;

        const lambda = Math.atan2(
            sineSigma * c.sineHeading,
            cc - ss * c.cosineHeading
        );

        const l =
            lambda -
            this._computeDeltaLambda(
                c.f,
                c.sineAlpha,
                c.cosineSquaredAlpha,
                sigma,
                sineSigma,
                cosineSigma,
                cosineTwiceSigmaMidpoint
            );

        return SSmap.Cartographic.create(this.start.longitude + l, latitude, 0);
    }
}

/**
 * 计算垂直观测矩形的最佳相机位置(3D)
 * @param {Rectangle} rectangle 目标矩形范围
 * @returns {Cartesian3} 最佳相机位置
 */
function rectangleCameraPosition3D(rectangle) {
    const defaultRF = {
        direction: SSmap.Cartesian3.create(0, 0, 0),
        right: SSmap.Cartesian3.create(0, 0, 0),
        up: SSmap.Cartesian3.create(0, 0, 0),
    };
    const cameraRF = defaultRF;

    let north = rectangle.north;
    let south = rectangle.south;
    let east = rectangle.east;
    let west = rectangle.west;

    // 处理跨越日界线的情况
    if (west > east) {
        east += TWO_PI;
    }

    const longitude = (west + east) * 0.5;
    let latitude;

    // 计算中心纬度
    // 如果矩形跨度较小且未接近极点,直接取平均
    // 否则使用大地线插值计算更精确的几何中心
    if (
        south < -PI_OVER_TWO + RADIANS_PER_DEGREE &&
        north > PI_OVER_TWO - RADIANS_PER_DEGREE
    ) {
        latitude = 0.0;
    } else {
        const northCartographic = SSmap.Cartographic.create(
            longitude,
            north,
            0.0
        );
        const southCartographic = SSmap.Cartographic.create(
            longitude,
            south,
            0.0
        );

        const geodesic = new GeodesicPath(northCartographic, southCartographic);
        const centerResult = geodesic.interpolate(0.5);
        latitude = centerResult.latitude;
    }

    const centerCartographic = SSmap.Cartographic.create(
        longitude,
        latitude,
        0.0
    );
    const ellipsoid = SSmap.Ellipsoid.WGS84();
    const center = ellipsoid.cartographicToCartesian(centerCartographic);

    // 计算矩形各角点和中心点在笛卡尔坐标系下的位置
    const points = [
        SSmap.Cartographic.create(east, north, 0), // NE
        SSmap.Cartographic.create(west, north, 0), // NW
        SSmap.Cartographic.create(longitude, north, 0), // N-Center
        SSmap.Cartographic.create(longitude, south, 0), // S-Center
        SSmap.Cartographic.create(east, south, 0), // SE
        SSmap.Cartographic.create(west, south, 0), // SW
    ].map((cart) => {
        const cartesian = ellipsoid.cartographicToCartesian(cart);
        return CartesianMath.subtract(cartesian, center); // 转换为相对于中心的向量
    });

    const [
        northEast,
        northWest,
        northCenter,
        southCenter,
        southEast,
        southWest,
    ] = points;

    // 构建相机局部坐标系 (Look At Center)
    // direction: 地表法线反向 (指向地心)
    cameraRF.direction = CartesianMath.minus(
        ellipsoid.geodeticSurfaceNormal(center)
    );

    // right: direction X (0,0,1) -> 东方
    cameraRF.right = CartesianMath.cross(
        cameraRF.direction,
        SSmap.Cartesian3.create(0, 0, 1)
    ).normalize();

    // up: right X direction -> 北方
    cameraRF.up = CartesianMath.cross(cameraRF.right, cameraRF.direction);

    // 计算所需的最小距离 d
    const camera = GlobalViewer.scene.mainCamera;
    const tanPhi = Math.tan(camera.fovy * 0.5 * RADIANS_PER_DEGREE);
    const tanTheta = camera.aspectRatio * tanPhi;

    // 检查所有关键点,取最大距离以确保包围盒在视锥体内
    let d = Math.max(
        computeD(cameraRF.direction, cameraRF.up, northWest, tanPhi),
        computeD(cameraRF.direction, cameraRF.up, southEast, tanPhi),
        computeD(cameraRF.direction, cameraRF.up, northEast, tanPhi),
        computeD(cameraRF.direction, cameraRF.up, southWest, tanPhi),
        computeD(cameraRF.direction, cameraRF.up, northCenter, tanPhi),
        computeD(cameraRF.direction, cameraRF.up, southCenter, tanPhi),
        computeD(cameraRF.direction, cameraRF.right, northWest, tanTheta),
        computeD(cameraRF.direction, cameraRF.right, southEast, tanTheta),
        computeD(cameraRF.direction, cameraRF.right, northEast, tanTheta),
        computeD(cameraRF.direction, cameraRF.right, southWest, tanTheta),
        computeD(cameraRF.direction, cameraRF.right, northCenter, tanTheta),
        computeD(cameraRF.direction, cameraRF.right, southCenter, tanTheta)
    );

    // 如果矩形跨越赤道,还需要检查赤道上的点,因为那是投影最宽处
    if (south < 0 && north > 0) {
        const equatorPoints = [
            SSmap.Cartographic.create(west, 0.0, 0.0),
            SSmap.Cartographic.create(east, 0.0, 0.0),
        ];

        equatorPoints.forEach((pt) => {
            let pos = ellipsoid.cartographicToCartesian(pt);
            pos = CartesianMath.subtract(pos, center);
            d = Math.max(
                d,
                computeD(cameraRF.direction, cameraRF.up, pos, tanPhi),
                computeD(cameraRF.direction, cameraRF.right, pos, tanTheta)
            );
        });
    }

    // 返回最终位置:中心点沿相机反方向移动距离 d
    return CartesianMath.adding(
        center,
        CartesianMath.vectorByNumber(cameraRF.direction, -d)
    );
}

/**
 * 根据指定俯仰角(Pitch)调整相机位置
 * 1. 先计算垂直观测位置
 * 2. 绕目标中心点旋转相机到指定俯仰角
 * @param {Object} params 参数对象
 * @param {Object} params.boundingBox 矩形范围 {west, south, east, north}
 * @param {Number} params.pitch 俯仰角 (角度值,如 -45)
 * @param {Number} params.altitude 高度偏移
 * @returns {Vector3} 调整后的世界坐标位置
 */
function rectChangePitch({ boundingBox, pitch, altitude }) {
    const ellipsoid = SSmap.Ellipsoid.WGS84();

    let rectangle;

    if (
        boundingBox.west ||
        boundingBox.south ||
        boundingBox.east ||
        boundingBox.north
    ) {
        rectangle = SSmap.Rectangle.fromDegrees(
            boundingBox.west,
            boundingBox.south,
            boundingBox.east,
            boundingBox.north
        );
    }
    if (Array.isArray(boundingBox)) {
        rectangle = SSmap.Rectangle.fromDegrees(
            boundingBox[0],
            boundingBox[1],
            boundingBox[2],
            boundingBox[3]
        );
    }

    // 获取垂直视角的相机初始位置坐标
    const position = rectangleCameraPosition3D(rectangle);
    let center = rectangle.center();

    if (altitude) {
        center.height = altitude;
    }
    center = center.toCartesian3();

    // 根据矩形中心点构建局部坐标系矩阵 (ENU: East-North-Up)
    const ENUToWorld = ellipsoid.eastNorthUpToFixedFrame(center); // Matrix4
    const worldToENU = ENUToWorld.inverted(); // Matrix4

    // 将相机初始位置坐标转换到局部坐标系
    const localValue = SSmap.Matrix4.multiplyByVector3(
        worldToENU,
        position.toVector3()
    ); // Vector3

    // 构造旋转四元数:绕 X 轴旋转 (90 + pitch) 度
    // 垂直向下是 -90度,此时 rot 为 0度。
    const rot = SSmap.Quaternion.fromAxisAndAngle(
        SSmap.Vector3.create(1, 0, 0),
        90 + pitch
    ); // Quaternion
    const rotMatrix = rot.toRotationMatrix(); // Matrix3

    // 应用旋转矩阵到相机初始位置局部坐标,得到旋转后的局部坐标
    const localValue2 = SSmap.Matrix3.multiplyByVector3(rotMatrix, localValue); // Vector3

    // 转换回世界坐标
    const worldValue = SSmap.Matrix4.multiplyByVector3(ENUToWorld, localValue2);
    return worldValue;
}

/**
 * 执行飞行操作:飞向矩形区域并保持指定俯仰角
 * @param {Object} options 配置项
 * @param {Object} options.boundingBox 矩形范围
 * @param {Number} options.pitch 俯仰角 (默认 -90)
 * @param {Number} options.duration 飞行时长 (秒)
 * @param {Number} options.altitude 矩形高度
 */
function flyToRectChangePitch({
    boundingBox,
    pitch = -90,
    duration = 1,
    altitude = 0,
}) {
    const orientation = {
        heading: 0,
        pitch: pitch,
        roll: 0,
    };

    const camera = GlobalViewer.scene.mainCamera;
    const cameraController = camera.cameraController();

    // 计算目标位置
    const position = rectChangePitch({
        boundingBox,
        pitch: orientation.pitch,
        altitude,
    }); // Vector3

    // 执行飞行
    cameraController.flyTo(
        position,
        duration,
        orientation.heading,
        orientation.pitch,
        orientation.roll
    );
}

export { flyToRectChangePitch };
// 使用示例:龙华区包围盒
// flyToRectChangePitch({
//     boundingBox: [113.998592578216901, 22.790307396489528, 114.059972774468775, 22.570420463846865],
//     pitch: -60,
//     duration: 2,
// });

技术亮点

  1. 采用 Vincenty 公式进行高精度大地测量计算,适应椭球地球模型
  2. 完整考虑视锥体可见性,确保目标区域完全显示
  3. 基于局部坐标系的旋转计算,保证视角调整的准确性
  4. 支持跨日界线区域和近极点区域的特殊处理

通过这套方案,可以轻松实现地理信息系统中对任意矩形区域的自动定位与相机控制,为片区管理、规划分析等场景提供强大支持。

相关推荐
江公望1 小时前
CSS variable 10分钟讲清楚
前端·css
随风一样自由1 小时前
React中实现iframe嵌套登录页面:跨域与状态同步解决方案探讨
javascript·react.js·ecmascript
Chicheng_MA1 小时前
OpenWrt WebUI 交互架构深度解析
javascript·lua·openwrt
pale_moonlight1 小时前
九、Spark基础环境实战(下)
大数据·javascript·spark
|晴 天|1 小时前
前端安全入门:XSS 与 CSRF 的攻与防
前端·安全·xss
黛色正浓1 小时前
【React】ReactRouter记账本案例实现
前端·react.js·前端框架
可爱又迷人的反派角色“yang”1 小时前
Mysql数据库(一)
运维·服务器·前端·网络·数据库·mysql·nginx
Aerelin1 小时前
爬虫图片采集(自动化)
开发语言·前端·javascript·爬虫·python·html
Highcharts.js1 小时前
Renko Charts|金融图表之“砖形图”
java·前端·javascript·金融·highcharts·砖型图·砖形图