cesium 根据照片的分辨率和经纬度、高度 实现照片映射到地图上

137 阅读4分钟

Cesium 照片映射到地图的完整实现

在 Cesium 中实现照片映射需要根据照片的元数据(经纬度、高度、方向、焦距等)将照片正确投影到地球表面上。以下是完整的实现方案:

1. 基本照片映射

class PhotoMapper {
    constructor(viewer) {
        this.viewer = viewer;
        this.photoEntities = [];
    }

    /**
     * 添加照片到地图
     * @param {Object} photoData 照片数据
     * @param {string} photoData.url 图片URL
     * @param {number} photoData.longitude 经度(度)
     * @param {number} photoData.latitude 纬度(度)
     * @param {number} photoData.altitude 高度(米)
     * @param {number} photoData.heading 朝向(度,0-360)
     * @param {number} photoData.pitch 俯仰角(度)
     * @param {number} photoData.roll 翻滚角(度)
     * @param {number} photoData.width 图片宽度(像素)
     * @param {number} photoData.height 图片高度(像素)
     * @param {number} photoData.focalLength 焦距(毫米)
     * @param {number} photoData.sensorWidth 传感器宽度(毫米)
     */
    async addPhoto(photoData) {
        try {
            // 计算视野范围
            const frustum = this.calculateFrustum(photoData);
            
            // 创建照片实体
            const entity = this.createPhotoEntity(photoData, frustum);
            
            this.photoEntities.push(entity);
            return entity;
        } catch (error) {
            console.error('添加照片失败:', error);
            throw error;
        }
    }

    /**
     * 计算照片的视锥体
     */
    calculateFrustum(photoData) {
        const { longitude, latitude, altitude, heading, pitch, roll, 
                width, height, focalLength, sensorWidth } = photoData;

        // 计算视野角度
        const hFov = 2 * Math.atan(sensorWidth / (2 * focalLength));
        const vFov = 2 * Math.atan((sensorWidth * height / width) / (2 * focalLength));

        // 转换为弧度
        const headingRad = Cesium.Math.toRadians(heading);
        const pitchRad = Cesium.Math.toRadians(pitch);
        const rollRad = Cesium.Math.toRadians(roll);

        return {
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude, altitude),
            orientation: Cesium.Transforms.headingPitchRollQuaternion(
                Cesium.Cartesian3.fromDegrees(longitude, latitude, altitude),
                new Cesium.HeadingPitchRoll(headingRad, pitchRad, rollRad)
            ),
            hFov: hFov,
            vFov: vFov
        };
    }

    /**
     * 创建照片实体
     */
    createPhotoEntity(photoData, frustum) {
        const { url, width, height } = photoData;

        return this.viewer.entities.add({
            name: '照片映射',
            position: frustum.position,
            orientation: frustum.orientation,
            ellipse: {
                semiMinorAxis: this.calculateCoverageRadius(frustum),
                semiMajorAxis: this.calculateCoverageRadius(frustum),
                material: new Cesium.ImageMaterialProperty({
                    image: url,
                    transparent: true
                }),
                height: 0,
                extrudedHeight: 10 // 轻微突出显示
            },
            description: this.createDescription(photoData)
        });
    }

    /**
     * 计算覆盖半径
     */
    calculateCoverageRadius(frustum) {
        // 简化的覆盖范围计算
        const distance = 1000; // 假设拍摄距离
        return distance * Math.tan(frustum.hFov / 2);
    }

    /**
     * 创建描述信息
     */
    createDescription(photoData) {
        return `
            <div class="photo-info">
                <img src="${photoData.url}" style="max-width: 200px; max-height: 150px;">
                <p>分辨率: ${photoData.width} × ${photoData.height}</p>
                <p>位置: ${photoData.latitude.toFixed(6)}, ${photoData.longitude.toFixed(6)}</p>
                <p>高度: ${photoData.altitude}米</p>
            </div>
        `;
    }

    /**
     * 清除所有照片
     */
    clearAllPhotos() {
        this.photoEntities.forEach(entity => {
            this.viewer.entities.remove(entity);
        });
        this.photoEntities = [];
    }
}

2. 使用 EXIF 数据自动映射

class ExifPhotoMapper extends PhotoMapper {
    constructor(viewer) {
        super(viewer);
    }

    /**
     * 从EXIF数据添加照片
     */
    async addPhotoFromExif(imageFile, imageUrl) {
        try {
            // 解析EXIF数据
            const exifData = await this.parseExifData(imageFile);
            
            // 转换为照片数据格式
            const photoData = this.exifToPhotoData(exifData, imageUrl);
            
            // 添加照片
            return await this.addPhoto(photoData);
        } catch (error) {
            console.error('EXIF照片映射失败:', error);
            throw error;
        }
    }

    /**
     * 解析EXIF数据
     */
    async parseExifData(imageFile) {
        // 这里可以使用exifr或其他EXIF解析库
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = function(e) {
                // 实际项目中应该使用exifr.parse()
                const fakeExifData = {
                    GPSLatitude: [39, 54, 54],
                    GPSLongitude: [116, 24, 14.4],
                    GPSAltitude: 50,
                    GPSImgDirection: 45,
                    FocalLength: 35,
                    // 其他EXIF字段...
                };
                resolve(fakeExifData);
            };
            reader.readAsArrayBuffer(imageFile);
        });
    }

    /**
     * EXIF数据转换为照片数据
     */
    exifToPhotoData(exifData, imageUrl) {
        // 转换GPS坐标
        const latitude = this.convertGpsToDecimal(
            exifData.GPSLatitude[0],
            exifData.GPSLatitude[1],
            exifData.GPSLatitude[2]
        );
        
        const longitude = this.convertGpsToDecimal(
            exifData.GPSLongitude[0],
            exifData.GPSLongitude[1],
            exifData.GPSLongitude[2]
        );

        return {
            url: imageUrl,
            longitude: longitude,
            latitude: latitude,
            altitude: exifData.GPSAltitude || 0,
            heading: exifData.GPSImgDirection || 0,
            pitch: 0,
            roll: 0,
            width: 4000, // 从EXIF获取或默认值
            height: 3000,
            focalLength: exifData.FocalLength || 35,
            sensorWidth: 36 // 全画幅传感器宽度
        };
    }

    /**
     * GPS坐标转换
     */
    convertGpsToDecimal(degrees, minutes, seconds) {
        return degrees + minutes / 60 + seconds / 3600;
    }
}

3. 精确的照片投影映射

class PrecisePhotoMapper extends PhotoMapper {
    /**
     * 精确的照片投影
     */
    createPrecisePhotoEntity(photoData, frustum) {
        const corners = this.calculatePhotoCorners(photoData, frustum);
        
        return this.viewer.entities.add({
            name: '精确照片映射',
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy(
                    corners.map(corner => Cesium.Cartesian3.fromRadians(
                        corner.longitude, corner.latitude, corner.height
                    ))
                ),
                material: photoData.url,
                height: 0,
                classificationType: Cesium.ClassificationType.TERRAIN
            }
        });
    }

    /**
     * 计算照片四个角坐标
     */
    calculatePhotoCorners(photoData, frustum) {
        const { hFov, vFov } = frustum;
        const distance = photoData.altitude / Math.sin(Cesium.Math.toRadians(photoData.pitch));
        
        const corners = [
            { x: -1, y: 1 },  // 左上
            { x: 1, y: 1 },   // 右上
            { x: 1, y: -1 },  // 右下
            { x: -1, y: -1 } // 左下
        ];

        return corners.map(corner => {
            const direction = this.calculateRayDirection(
                corner.x * Math.tan(hFov / 2),
                corner.y * Math.tan(vFov / 2),
                frustum.orientation
            );
            
            const ray = new Cesium.Ray(frustum.position, direction);
            const intersection = this.viewer.scene.globe.pick(ray, this.viewer.scene);
            
            if (intersection) {
                const cartographic = Cesium.Cartographic.fromCartesian(intersection);
                return {
                    longitude: cartographic.longitude,
                    latitude: cartographic.latitude,
                    height: cartographic.height
                };
            }
            
            // 如果没有交点,使用近似计算
            return this.approximateCorner(photoData, corner);
        });
    }

    /**
     * 计算射线方向
     */
    calculateRayDirection(xOffset, yOffset, orientation) {
        const direction = new Cesium.Cartesian3(0, 0, -1); // 相机前方
        const rotation = Cesium.Matrix3.fromQuaternion(orientation);
        Cesium.Matrix3.multiplyByVector(rotation, direction, direction);
        
        // 应用偏移
        const right = new Cesium.Cartesian3();
        Cesium.Cartesian3.cross(direction, Cesium.Cartesian3.UNIT_Z, right);
        Cesium.Cartesian3.normalize(right, right);
        
        const up = new Cesium.Cartesian3();
        Cesium.Cartesian3.cross(right, direction, up);
        Cesium.Cartesian3.normalize(up, up);
        
        const result = new Cesium.Cartesian3();
        Cesium.Cartesian3.add(
            direction,
            Cesium.Cartesian3.multiplyByScalar(right, xOffset, new Cesium.Cartesian3()),
            result
        );
        Cesium.Cartesian3.add(
            result,
            Cesium.Cartesian3.multiplyByScalar(up, yOffset, new Cesium.Cartesian3()),
            result
        );
        
        return Cesium.Cartesian3.normalize(result, new Cesium.Cartesian3());
    }
}

4. 完整的使用示例

// 初始化Cesium Viewer
const viewer = new Cesium.Viewer('cesiumContainer', {
    terrainProvider: Cesium.createWorldTerrain()
});

// 创建照片映射器实例
const photoMapper = new PhotoMapper(viewer);

// 示例照片数据
const samplePhoto = {
    url: 'path/to/photo.jpg',
    longitude: 116.404,
    latitude: 39.915,
    altitude: 100,
    heading: 45,
    pitch: -30,
    roll: 0,
    width: 4000,
    height: 3000,
    focalLength: 35,
    sensorWidth: 36
};

// 添加照片
photoMapper.addPhoto(samplePhoto).then(entity => {
    console.log('照片添加成功');
    
    // 飞到照片位置
    viewer.flyTo(entity, {
        duration: 2,
        offset: new Cesium.HeadingPitchRange(0, -0.5, 1000)
    });
});

// 添加多个照片
const photos = [
    { /* 照片数据1 */ },
    { /* 照片数据2 */ },
    { /* 照片数据3 */ }
];

photos.forEach(async (photo, index) => {
    try {
        await photoMapper.addPhoto(photo);
        console.log(`照片 ${index + 1} 添加成功`);
    } catch (error) {
        console.error(`照片 ${index + 1} 添加失败:`, error);
    }
});

// 清除所有照片
document.getElementById('clear-btn').addEventListener('click', () => {
    photoMapper.clearAllPhotos();
});

5. 样式优化

.photo-info {
    padding: 10px;
    background: white;
    border-radius: 5px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.photo-info img {
    border-radius: 3px;
    margin-bottom: 10px;
}

.photo-info p {
    margin: 5px 0;
    font-size: 12px;
    color: #666;
}

注意事项

  1. 坐标系一致性:确保所有坐标使用 WGS84 坐标系
  2. 单位转换:注意角度单位(度/弧度)和长度单位的转换
  3. 性能优化:大量照片映射时需要考虑性能问题
  4. 错误处理:添加适当的错误处理和用户反馈
  5. 地形影响:考虑地形对照片投影的影响

这个完整的照片映射方案可以根据实际需求进行调整和扩展,支持从简单的椭圆显示到精确的投影映射等多种应用场景。