Appearance
测试 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/