ArcGIS JSAPI 高级教程 - 场景可视区域(SceneView visibleArea)显示以及过滤要素应用
本文主要介绍一下场景可视区域(SceneView visibleArea),一般用于过滤视野内可见部分,实际为相机视锥体在地面上的近似投影多边形。
首先介绍一下实现过程:
1. 主场景称为场景,右上角场景称为类鹰眼场景。
2. 创建包含可视区域的场景,并且添加地形图层,用于计算类鹰眼展示相机高度。
3. 添加要素图层,用于实现过滤功能。
4. 通过场景相机构建类鹰眼场景中的几何体、范围、虚拟相机对象等。
5. 通过监听事件,进行数据更新、要素过滤更新等。

本文包括 完整代码以及在线示例。
完整代码
html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<title>可视区域 | Sample | ArcGIS Maps SDK for JavaScript 4.33</title>
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
#viewDivSupport {
width: 25%;
height: 35%;
top: 5px;
right: 20px;
border: 2px solid black;
display: none;
}
:root {
--my-width: 100vw;
}
#divToggle {
display: none;
bottom: 0;
right: 5px;
}
.white-text {
color: white;
line-height: 0;
}
#myCustomGroup {
position: absolute;
top: 16px;
left: 64px;
}
</style>
<script type="module" src="https://openlayers.vip/arcgis_api/calcite-components/2.8.1/calcite.esm.js"></script>
<!-- 引入ArcGIS JS API样式和脚本 -->
<link rel="stylesheet" href="https://openlayers.vip/arcgis_api/4.33/esri/themes/light/main.css"/>
<script src="https://openlayers.vip/arcgis_api/4.33/init.js"></script>
<script>
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?f80a36f14f8a73bb0f82e0fdbcee3058";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</head>
<body>
<div id="viewDiv">
<div id="viewDivSupport"></div>
<div id="divToggle">
<calcite-label scale="s" layout="inline">
<p class="white-text">2D</p>
<calcite-switch id="3dToggle" scale="s"></calcite-switch>
<p class="white-text">3D</p>
</calcite-label>
</div>
</div>
<div id="myCustomGroup">
<calcite-block open heading="是否显示场景范围" id="renderNodeUI">
<calcite-label layout="inline">
关闭
<calcite-switch id="renderNodeToggle" checked></calcite-switch>
开启
</calcite-label>
</calcite-block>
<calcite-tile-group id="footprintSelectTileGroup" alignment="center">
<calcite-tile class="tile-container" heading="模拟建筑数量:">
<div slot="content-bottom" class="tile-content">
<calcite-chip></calcite-chip>
</div>
</calcite-tile>
</calcite-tile-group>
</div>
<script>
require([
"esri/layers/FeatureLayer",
"esri/layers/GraphicsLayer",
"esri/Graphic",
"esri/core/reactiveUtils",
"esri/geometry/Polygon",
"esri/views/SceneView",
"esri/geometry/Polyline",
"esri/core/promiseUtils",
"esri/symbols/IconSymbol3DLayer",
"esri/symbols/PointSymbol3D",
"esri/symbols/ObjectSymbol3DLayer",
"esri/geometry/Point",
"esri/Map",
"esri/smartMapping/renderers/color",
], (
FeatureLayer,
GraphicsLayer,
Graphic,
reactiveUtils,
Polygon,
SceneView,
Polyline,
promiseUtils,
IconSymbol3DLayer,
PointSymbol3D,
ObjectSymbol3DLayer,
Point,
Map,
colorRendererCreator,
) => {
const map = new Map({
ground: "world-elevation"
});
// 创建场景
const view = new SceneView({
container: "viewDiv",
map: map,
camera: {
position: [7.95442341, 46.48978665, 3407.29792],
heading: 351.99,
tilt: 18.52
}
});
// 控制开启范围显示
let activateExtent = true;
const renderNodeToggle = document.getElementById("renderNodeToggle");
renderNodeToggle.addEventListener("calciteSwitchChange", () => {
activateExtent = !activateExtent;
});
// 添加要素图层,统计展示过滤数量
const featureLayer = new FeatureLayer({
url: 'https://gs3d.geosceneonline.cn/server/rest/services/Hosted/ShangHaiBuilding/FeatureServer/0',
minScale: 0,
maxScale: 0,
outFields: ["*"],
})
map.add(featureLayer); // adds the layer to the map
// visualization based on field and normalization field
let colorParams = {
layer: featureLayer,
view: view,
field: "SHAPE__Area",
classificationMethod: "natural-breaks",
valueExpression: "$feature.SHAPE__Area",
theme: "high-to-low",
numClasses: 10,
symbolType: '3d-volumetric',
};
// when the promise resolves, apply the renderer to the layer
colorRendererCreator.createClassBreaksRenderer(colorParams)
// colorRendererCreator.createContinuousRenderer(colorParams)
.then(function (response) {
featureLayer.renderer = response.renderer;
});
// Keep track of the type of supportView
let isSupportViewTilted = false;
const cameraIconUrl = "https://openlayers.vip/examples/resources/cameraIcon.svg";
const camera3DObjectUrl = "https://openlayers.vip/examples/resources/cameraObject.glb";
view.when(function () {
view.extent = featureLayer.fullExtent;
// 创建类鹰眼场景
const supportView = new SceneView({
container: "viewDivSupport",
map: new Map(),
center: view.camera.position,
zoom: 13,
ui: {
components: []
},
constraints: {
tilt: {
max: 0 // Prevent the user from tilting the camera. This ensures the view remains a bird's-eye view (top-down), looking straight down
}
}
});
// Add the support view to the main view ui and make it visible
const supportViewElement = document.getElementById("viewDivSupport");
view.ui.add(supportViewElement, "manual");
supportViewElement.style.display = "flex";
// Add a view mode toggle to the supportView for switching between top-down and tilted perspectives
const divToggle = document.getElementById("divToggle");
supportView.ui.add(divToggle, "manual");
const toggle = document.getElementById("3dToggle");
toggle.addEventListener("calciteSwitchChange", (event) => {
handle3DToggle(event);
});
// 视椎体图层
const frustumGraphicsLayer = new GraphicsLayer({
elevationInfo: {
mode: "relative-to-scene"
},
visible: false
});
supportView.map.add(frustumGraphicsLayer);
// 可见区域图层
const visibleAreaGraphicsLayer = new GraphicsLayer({
elevationInfo: {
mode: "on-the-ground"
}
});
supportView.map.add(visibleAreaGraphicsLayer);
// 虚拟相机图层
const cameraGraphicLayer = new GraphicsLayer({
elevationInfo: {
mode: "on-the-ground"
}
});
supportView.map.add(cameraGraphicLayer);
// 相机对象
const cameraGraphic = new Graphic({
geometry: view.camera.position,
symbol: new PointSymbol3D({
symbolLayers: [
new IconSymbol3DLayer({
resource: {href: cameraIconUrl},
angle: view.camera.heading - 90
})
]
})
});
cameraGraphicLayer.add(cameraGraphic);
// Wait until the support view is ready
supportView.when(async () => {
// Show the top-down/tilted view toggle
divToggle.style.display = "flex";
// Wait for the layerView of the tree feature layer
const featureLayerView = await view.whenLayerView(featureLayer);
// Debounce the queries to the tree featureLayerView to avoid multiple
// requests being sent to the server while the user is interacting with the Scene
const debounceQueryTrees = promiseUtils.debounce(async () => {
try {
// 过滤要素
const featureSet = await featureLayerView.queryFeatures({
// 使用可见区域过滤
geometry: view.visibleArea,
returnGeometry: false,
outFields: ["floor"]
});
// 更新显示数量
const chip = document.querySelector("calcite-chip");
if (chip) {
chip.textContent = featureSet.features.length.toString();
}
} catch (error) {
console.error("query failed: ", error);
}
});
// 监听数据更新
reactiveUtils.when(
() => !featureLayerView.dataUpdating,
() => {
debounceQueryTrees().catch((error) => {
if (error.name === "AbortError") {
return;
}
console.error(error);
});
}
);
// 显示区域样式
const visibleAreaSymbol = {
type: "polygon-3d",
symbolLayers: [
{
type: "fill",
material: {color: "white"},
outline: {color: "white", width: 2},
pattern: {
type: "style",
style: "forward-diagonal"
},
},
]
};
// 获取可视区域
function getVisibleAreaGraphics(visibleArea, extent) {
const parts = visibleArea.rings.map(
(ring) => new Polygon({rings: [ring], spatialReference: visibleArea.spatialReference})
);
const visibleAreaPolygons = parts.map((part) => new Graphic({
geometry: part,
symbol: visibleAreaSymbol
}));
// 显示地图范围
if (activateExtent && extent) {
// extentBuilding
const polygonExtent = Polygon.fromExtent(extent)
polygonExtent.hasZ = false;
const polygonExtentGraphic = new Graphic({
geometry: polygonExtent,
symbol: {
type: "simple-fill",
color: [51, 51, 204, 0],
style: "solid",
outline: {
color: [0, 255, 255, 1],
width: 2
}
}
});
visibleAreaPolygons.push(polygonExtentGraphic);
}
return visibleAreaPolygons;
}
// 地形取样
let elevationSampler = view.groundView.elevationSampler;
// 监听可视区域变化
reactiveUtils.when(
() => view.visibleArea,
() => {
// Debounce the queries to the tree featureLayerView to avoid multiple requests
debounceQueryTrees().catch((error) => {
if (error.name === "AbortError") {
return;
}
console.error(error);
});
// Update the supportView
updateSceneView();
},
{
initial: true
}
);
// 监听场景更新
reactiveUtils.when(
() => !view.updating,
() => {
supportView.goTo({
// goTo the visibleArea changing the tilt for a better camera positioning
target: [view.visibleArea.extent],
tilt: isSupportViewTilted ? 60 : 0
});
}
);
// 更新类鹰眼场景显示数据
async function updateSceneView() {
frustumGraphicsLayer.removeAll();
visibleAreaGraphicsLayer.removeAll();
let {visibleArea, extent} = view;
const visibleAreaGraphics = getVisibleAreaGraphics(visibleArea, extent);
visibleAreaGraphicsLayer.addMany(visibleAreaGraphics);
// Get the elevation value from the elevationSampler starting from the camera position
const cameraPosition = view.camera.position;
const groundElevation = elevationSampler.elevationAt(cameraPosition.x, cameraPosition.y);
const relativeCameraHeight = cameraPosition.z - groundElevation;
// 更新相机姿态
cameraGraphic.geometry = new Point({
x: cameraPosition.x,
y: cameraPosition.y,
z: relativeCameraHeight,
spatialReference: view.visibleArea.spatialReference
});
cameraGraphic.symbol = updateCameraSymbol(view.camera.heading, view.camera.tilt);
// 可视区域样式
const lineSymbol = {
type: "simple-line",
color: [255, 255, 255],
width: 1.5,
};
// Iterate on the visibleArea rings
for (const ring of visibleArea.rings) {
for (const point of ring) {
// Create a Polyline connecting the camera position to visibleArea vertices
const connectingLine = new Polyline({
paths: [
[
[point[0], point[1], 0],
[cameraPosition.x, cameraPosition.y, relativeCameraHeight]
]
],
spatialReference: visibleArea.spatialReference
});
// Create a Graphic for the connecting line and add it its graphicsLayer
const lineGraphic = new Graphic({
geometry: connectingLine,
symbol: lineSymbol
});
frustumGraphicsLayer.add(lineGraphic);
}
}
}
});
// 切换类鹰眼场景二三维效果
function handle3DToggle(event) {
isSupportViewTilted = event.target.checked;
if (isSupportViewTilted) {
frustumGraphicsLayer.visible = true;
supportView.constraints.tilt = {};
cameraGraphicLayer.elevationInfo.mode = "relative-to-scene";
} else {
// supportView is in bird's-eye view mode (top-down)
frustumGraphicsLayer.visible = false;
supportView.constraints.tilt.max = 0;
cameraGraphicLayer.elevationInfo.mode = "on-the-ground";
}
// Get the proper symbol for the camera and then rotate it
cameraGraphic.symbol = updateCameraSymbol(view.camera.heading, view.camera.tilt);
// Center the supportView to the visibleArea extent
supportView.goTo({
target: [view.visibleArea.extent],
tilt: isSupportViewTilted ? 60 : 0
});
}
// 更新相机显示
function updateCameraSymbol(heading, tilt) {
if (isSupportViewTilted) {
return new PointSymbol3D({
symbolLayers: [
new ObjectSymbol3DLayer({
width: 15,
heading,
tilt: tilt - 90,
resource: {
href: camera3DObjectUrl
}
})
]
});
} else {
// supportView is in bird's-eye view mode (top-down)
return new PointSymbol3D({
symbolLayers: [
new IconSymbol3DLayer({
resource: {href: cameraIconUrl},
angle: heading - 90
})
]
});
}
}
})
});
</script>
</body>
</html>

在线示例
ArcGIS Maps SDK for JavaScript 在线示例:场景可视区域(visibleArea)显示以及过滤要素应用