Skip to content

测试 API 之加载 Arcgis 历史影像

演示视频

注意事项

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

支持平台

三维引擎:Cesium 所有版本

游戏引擎:Unreal

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

代码示例和注释和

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

说明:Arcgis 历史影像服务主要是基于 Arcgis 官方的影像数据

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;

        // 更新图层的函数
        function updateImageryLayer(timeID) {
            viewer.imageryLayers.removeAll(); // 移除所有现有的图层
            var imgLayer = new XbsjTileserArcgisHisImageryProvider({
                indexTimeID: timeID
            });
            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.time.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.time.length > 0) {
                document.getElementById('dateDropdownContainer').style.display = 'block';
            } else {
                document.getElementById('dateDropdownContainer').style.display = 'none';
            }

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

        //根据时间数组,找到时间ID
        function findTimeID(allTimes, time) {
            const timeIdMap = allTimes.time.reduce((map, time, index) => {
                map[time] = allTimes.timeID[index];
                return map;
            }, {});

            const selectedId = timeIdMap[time]; // 获取对应的 ID
            return selectedId;
        }
        // 初始化地图和监听相机移动事件
        function xbsjTileserArcgisHis() {
            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);
                }
            });
        }

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

历史影像效果图

核心代码说明

获取视角的时间表[时间和对应的 ID]

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

https://tileser.giiiis.com/arcgis/7/106/54 为例,获取所有时间信息。

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

结合上面的方法.获取视角中心瓦片的时间信息

javascript
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));

    let y = Math.floor((1 - Math.log(Math.tan(latitude * Math.PI / 180) + 1 / Math.cos(latitude * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, level));

    const allTimes = await arcgisXyzToAllInfo(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 + 1;
        }
    }

    return 20; // 默认级别
}

封装 Arcgis 历史影像 ImageryProvider

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

javascript
class XbsjTileserArcgisHisImageryProvider extends Cesium.UrlTemplateImageryProvider {
    constructor(options) {
        // 设置默认的 minimumLevel 和 tilingScheme
        options.url = "https://wayback.maptiles.arcgis.com/";
        options.minimumLevel = 1;
        options.maximumLevel = 18;

        super(options);

        // 初始化 indexTime 属性
        this.indexTimeID = options.indexTimeID || 0; // 默认值为 1
    }

    async requestImage(x, y, level, request) {
        try {
            let imageUrl;
            if (this.indexTime !== 0) {
                imageUrl = this.buildImageUrl(this.indexTimeID, x, y, level);
            } else {
                imageUrl = `https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/WMTS/1.0.0/default028mm/MapServer/tile/${level}/${y}/${x}`;
            }
            return Cesium.ImageryProvider.loadImage(this, imageUrl);
        } catch (error) {
            return undefined;
        }
    }

    buildImageUrl(indexTimeID, x, y, level) {
        // 构建并返回基于时间信息的图像 URL
        // 这里的实现取决于你的 URL 结构和如何使用时间信息
        return `https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/WMTS/1.0.0/default028mm/MapServer/tile/${indexTimeID}/${level}/${y}/${x}`;
    }
}

加载历史影像

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

javascript
function updateImageryLayer(timeID) {
    viewer.imageryLayers.removeAll(); // 移除所有现有的图层
    var imgLayer = new XbsjTileserArcgisHisImageryProvider({
        indexTimeID: timeID
    });
    viewer.imageryLayers.addImageryProvider(imgLayer);
}
//创建插件中,历史影像核心对象XbsjTileserArcgisHisImageryProvider,
//配置indexTime为arcgis地理自带的ID,

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

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

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

javascript
// 初始化地图和监听相机移动事件
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);
    }
});

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

显示时间

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

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.time.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.time.length > 0) {
        document.getElementById('dateDropdownContainer').style.display = 'block';
    } else {
        document.getElementById('dateDropdownContainer').style.display = 'none';
    }

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

//根据时间数组,找到时间ID
function findTimeID(allTimes, time) {
    const timeIdMap = allTimes.time.reduce((map, time, index) => {
        map[time] = allTimes.timeID[index];
        return map;
    }, {});

    const selectedId = timeIdMap[time]; // 获取对应的 ID
    return selectedId;
}

UE 等其他说明

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

在线效果

测试地址

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