Skip to content

测试 API 之加载历史影像

演示视频

注意事项

在实际的项目开发中,影像、地形是所有地理信息相关可视化的基础数据。 TileSer 离线地图服务系统,可以用于在内网发布地图影像、历史影像、地形高程、3D Tiles 模型、Osgb 模型、街景等。 其他内容,详见 API 文档。 本文将以 Cesium 为例,讲解如何调用调用无水印历史高清影像。

支持平台

三维引擎:Cesium 所有版本

游戏引擎:Unreal

其他:其他二维码等平台,目前没有主动适配(用处较小,接受定制)

代码示例和注释

本示例将展示 Cesium 如何添加历史影像服务。

说明:最新影像和无水印默认影像有何区别? 最新影像,默认加载的是最新影像。不考虑清晰度、云层等遮挡情况。 默认影像,会考虑清晰度、云层遮挡,默认选择最高清和优质的影像。

HTML

html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>XbsjTileserImageryProvider示例</title>
    <script type="text/javascript" src="https://cesium.com/downloads/cesiumjs/releases/1.112/Build/Cesium/Cesium.js"></script>
    <script src="./tileserHis.js"></script>
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/cesium/1.109.0/Widgets/widgets.css">
    <style>
        html,
        body,
        #cesiumContainer {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            overflow: hidden;
        }

        .dateDropdownContainer {
            position: absolute;
            z-index: 99999;
            top: 60px;
            /* 调整这个值以避免与按钮组重叠 */
            left: 20px;
        }
    </style>
</head>

Javscript

javascript
<body>
    <div id="cesiumContainer">
        <div id="dateDropdownContainer" class="dateDropdownContainer" style="display: none;">
            <select id="dateDropdown"></select>
        </div>

    </div>
    <script>
        const viewer = new Cesium.Viewer('cesiumContainer', {});
        // 全局变量,存储当前选中的时间
        let currentSelectedTime;

        // 初始化地图和监听相机移动事件
        updateImageryLayer(0); // 初始化图层
        getAndDisplayInitialTimeData(); // 获取初始时间并显示
        let isCameraMoving = false;

        viewer.camera.moveStart.addEventListener(() => {
            isCameraMoving = true;
        });
        viewer.camera.moveEnd.addEventListener(async () => {
            if (isCameraMoving) {
                isCameraMoving = false;
                const allTimes = await getCurrentTileCoordinates(viewer);
                displayDatesInDropdown(allTimes);
            }
        });

        // 更新图层的函数
        function updateImageryLayer(decimalTime) {
            viewer.imageryLayers.removeAll(); // 移除所有现有的图层
            var imgLayer = new XbsjTileserHisImageryProvider({
                indexTime: decimalTime
            });
            viewer.imageryLayers.addImageryProvider(imgLayer);
        }

        // 显示下拉菜单的函数
        function displayDatesInDropdown(allTimes) {
            const dropdown = document.getElementById('dateDropdown');
            dropdown.innerHTML = ''; // 清空现有选项

            let closestIndex = -1;
            let closestDiff = Infinity;

            // 函数:计算两个日期字符串之间的天数差异
            function getDaysDifference(dateStr1, dateStr2) {
                const date1 = new Date(dateStr1);
                const date2 = new Date(dateStr2);
                const diffTime = Math.abs(date2 - date1);
                return diffTime / (1000 * 60 * 60 * 24);
            }

            allTimes.forEach((time, index) => {
                const option = document.createElement('option');
                option.value = time;
                option.textContent = time;
                dropdown.appendChild(option);

                // 寻找与 currentSelectedTime 最接近的时间
                const diff = getDaysDifference(time, currentSelectedTime);
                if (diff < closestDiff) {
                    closestDiff = diff;
                    closestIndex = index;
                }
            });

            // 如果找到最接近的时间,则选择它
            if (closestIndex !== -1) {
                dropdown.selectedIndex = closestIndex;// 更新当前选中的时间
            }

            if (allTimes.length > 0) {
                document.getElementById('dateDropdownContainer').style.display = 'block';
            } else {
                document.getElementById('dateDropdownContainer').style.display = 'none';
            }

            dropdown.onchange = function () {
                currentSelectedTime = dropdown.value; // 更新当前选中的时间
                updateImageryLayer(currentSelectedTime);
            };
        }

        // 获取初始时间并显示的函数
        async function getAndDisplayInitialTimeData() {
            const initialTimes = await getCurrentTileCoordinates(viewer); // 获取时间数据
            displayDatesInDropdown(initialTimes); // 显示时间数据
        }
    </script>
</body>

历史影像效果图

核心代码说明

获取视角的时间表

javascript
//根据瓦片xyz值,获取瓦片的时间信息
async function xyzToAllInfo(x, y, z) {
    const response = await fetch(`https://tileser.giiiis.com/xyzinfo/${z}/${x}/${y}`);
    const jsonData = response.json();
    return jsonData;
}

https://tileser.giiiis.com/xyzinfo/14/27317/5444 为例,获取所有时间信息。

获取视角中心瓦片的时间信息

结合上面的方法

javascript
//获取cesium视口范围的信息。
async function getCurrentTileCoordinates(viewer) {
    const scene = viewer.scene;
    const ellipsoid = scene.globe.ellipsoid;
    const camera = scene.camera;

    // 获取相机的经纬度
    const cameraPositionCartographic = ellipsoid.cartesianToCartographic(camera.position);
    const longitude = Cesium.Math.toDegrees(cameraPositionCartographic.longitude);
    const latitude = Cesium.Math.toDegrees(cameraPositionCartographic.latitude);

    // 计算缩放级别 (Z)
    const cameraHeight = cameraPositionCartographic.height;
    const level = altToZoom(cameraHeight);

    // 转换经纬度为瓦片坐标 (X, Y)
    let x = Math.floor((longitude + 180) / 360 * Math.pow(2, level + 1));
    let y = Math.floor((90 - latitude) / 180 * Math.pow(2, level));

    //获取信息XYZ信息,查看时间进行返回
    const allTimes = await xyzToAllInfo(x, y, level);

    return allTimes;
}

//3D级数转为2D高,根据
function altToZoom(cameraHeight) {
    const levels = [
        { maxAlt: 250000000, level: 0 },
        { maxAlt: 25000000, level: 1 },
        { maxAlt: 9000000, level: 2 },
        { maxAlt: 7000000, level: 3 },
        { maxAlt: 4400000, level: 4 },
        { maxAlt: 2000000, level: 5 },
        { maxAlt: 1000000, level: 6 },
        { maxAlt: 493977, level: 7 },
        { maxAlt: 218047, level: 8 },
        { maxAlt: 124961, level: 9 },
        { maxAlt: 56110, level: 10 },
        { maxAlt: 40000, level: 11 },
        { maxAlt: 13222, level: 12 },
        { maxAlt: 7000, level: 13 },
        { maxAlt: 4000, level: 14 },
        { maxAlt: 2500, level: 15 },
        { maxAlt: 1500, level: 16 },
        { maxAlt: 600, level: 17 },
        { maxAlt: 250, level: 18 },
        { maxAlt: 150, level: 19 },
        { maxAlt: 50, level: 20 }
    ];

    for (const { maxAlt, level } of levels) {
        if (cameraHeight >= maxAlt) {
            return level;
        }
    }

    return 20; // 默认级别
}

封装历史影像 ImageryProvider

继承 UrlTemplateImageryProvider 主要是为了可以方便的改时间信息。如果不封装这个。直接修改 url 地址也是一样的。

javascript
class XbsjTileserHisImageryProvider extends Cesium.UrlTemplateImageryProvider {
    constructor(options) {
        // 设置默认的 minimumLevel 和 tilingScheme
        options.url = "https://tileser.giiiis.com/timetile/";
        options.minimumLevel = 1;
        options.maximumLevel = 18;
        options.tilingScheme = new Cesium.GeographicTilingScheme();

        super(options);

        // 初始化 indexTime 属性
        this.indexTime = options.indexTime || 0; // 默认值为0
    }
    async requestImage(x, y, level, request) {
        // 重写 requestImage 方法以支持异步操作

        // 异步获取时间信息
        try {
            // 根据获取的时间信息构建新的 URL
            let imageUrl = this.buildImageUrl(this.indexTime, x, y, level);
            return Cesium.ImageryProvider.loadImage(this, imageUrl);
        } catch (error) {
            return undefined;
        }
    }

    buildImageUrl(indexTime, x, y, level) {
        return `http://tileser.giiiis.com/timetile/${indexTime}/${level}/${x}/${y}.jpg`;
    }
}

根据时间去加载历史影像

封装 XbsjTileserHisImageryProvider,就很方便修改 url 的信息。然后封装一个根据时间传参,穿件地图的方法,

javascript
function updateImageryLayer(decimalTime) {
    viewer.imageryLayers.removeAll(); // 移除所有现有的图层
    var imgLayer = new XbsjTileserHisImageryProvider({
        indexTime: decimalTime
    });
    viewer.imageryLayers.addImageryProvider(imgLayer);
}
//创建插件中,历史影像核心对象XbsjTileserHisImageryProvider,配置indexTime为0,即可加载最新影像
//当传值为allTimes的数组的某一个值,如果"2021-9-28",即可修改时间。

请注意!请注意! 切换影像或者时间,请务必移除全部影像! viewer.imageryLayers.removeAll(); // 移除所有现有的图层

更新影像,交互,较为复杂。

简单来说就是根据相机变化,await getCurrentTileCoordinates(viewer);获取时间信息。然后交给下拉菜单进行展示。

javascript
// 全局变量,存储当前选中的时间和上一次选中的时间
let currentSelectedTime;

updateImageryLayer(0); //初始化图层,默认为0,即刚开始默认加载最新时间
getAndDisplayInitialTimeData(); //获取初始时间并显示,这里主要作用直接获取全部时间,然后显示到下拉菜单的控件中
//该处,用户可以根据业务需要,重写displayDatesInDropdown函数。

//**//            
let isCameraMoving = false;
viewer.camera.moveStart.addEventListener(() => {
    isCameraMoving = true;
});
viewer.camera.moveEnd.addEventListener(async () => {
    if (isCameraMoving) {
        isCameraMoving = false;
        const allTimes = await getCurrentTileCoordinates(viewer);
        displayDatesInDropdown(allTimes);
    }
});
//以上代码核心作用是监听相机改变事件,也就是当:相机位移,即获取最新的全部时间。然后displayDatesInDropdown进行判定和显示,
//**// 

async function getAndDisplayInitialTimeData() {
    const initialTimes = await getCurrentTileCoordinates(viewer); // 获取时间数据
    displayDatesInDropdown(initialTimes); // 显示时间数据
}

显示时间,较为复杂

根据时间表,进行展示,同时!最关键的是,onchange 事件,也就是点击选择时间就切换整个地球的时间。 以下代码建议全保留

javascript
// 显示下拉菜单的函数
        function displayDatesInDropdown(allTimes) {
            const dropdown = document.getElementById('dateDropdown');
            dropdown.innerHTML = ''; // 清空现有选项

            let closestIndex = -1;
            let closestDiff = Infinity;

            // 函数:计算两个日期字符串之间的天数差异
            function getDaysDifference(dateStr1, dateStr2) {
                const date1 = new Date(dateStr1);
                const date2 = new Date(dateStr2);
                const diffTime = Math.abs(date2 - date1);
                return diffTime / (1000 * 60 * 60 * 24);
            }

            allTimes.forEach((time, index) => {
                const option = document.createElement('option');
                option.value = time;
                option.textContent = time;
                dropdown.appendChild(option);

                // 寻找与 currentSelectedTime 最接近的时间
                const diff = getDaysDifference(time, currentSelectedTime);
                if (diff < closestDiff) {
                    closestDiff = diff;
                    closestIndex = index;
                }
            });

            // 如果找到最接近的时间,则选择它
            if (closestIndex !== -1) {
                dropdown.selectedIndex = closestIndex;// 更新当前选中的时间
            }

            if (allTimes.length > 0) {
                document.getElementById('dateDropdownContainer').style.display = 'block';
            } else {
                document.getElementById('dateDropdownContainer').style.display = 'none';
            }

            dropdown.onchange = function () {
                currentSelectedTime = dropdown.value; // 更新当前选中的时间
                updateImageryLayer(currentSelectedTime);
            };
        }

UE 等其他说明

历史影像同样可以在 ue 中实现,不过,因为相对更加复杂。尤其是获取相机信息。这个必须要在 UE 里实现。 为了方便试用,代码已经封装到 EarthSDK,详见 https://earthsdk.com/

如果不在意,获取时间麻烦,UE 加载需要转为地图引擎,比如 cesium for Unreal、Unity。

javascript
地址:https://tileser.giiiis.com/timetile/tms/{时间数值}/tilemapresource.xml

将上面的地址,复制到 Cesium World Terrain 的 CesiumTileMapServiceRasterOverlay 的 url 中

在线效果

测试地址

测试 API 之加载历史影像 has loaded