511 lines
17 KiB
JavaScript
511 lines
17 KiB
JavaScript
/*
|
||
* @Author: Wang jianLei
|
||
* @Date: 2022-05-17 21:49:28
|
||
* @Last Modified by: Wang JianLei
|
||
* @Last Modified time: 2022-05-19 22:08:14
|
||
*/
|
||
|
||
let ViewShed = function (sdk, canvasEleId) {
|
||
if (!sdk.viewer) throw new Error("no viewer object!");
|
||
let canvasEle = document.getElementById(canvasEleId);
|
||
if (!canvasEle) throw new Error("the canvas element is not exist");
|
||
this.canvasEle = canvasEle;
|
||
this.viewer = sdk.viewer;
|
||
this.handler = undefined;
|
||
this.lightCamera;
|
||
this.pyramid;
|
||
this.frustumPrimitive;
|
||
this.viewershedPolygon;
|
||
};
|
||
ViewShed.prototype = {
|
||
/**
|
||
* 初始化handler
|
||
*/
|
||
initHandler() {
|
||
if (this.handler) {
|
||
this.handler.destroy();
|
||
this.handler = undefined;
|
||
}
|
||
},
|
||
/**
|
||
* 开始执行视域分析
|
||
* @param {number} precision 精度,值越大创建耗时越长,建议在10~20之间
|
||
*/
|
||
createViewshed: function (precision) {
|
||
let $this = this;
|
||
let scene = $this.viewer.scene;
|
||
$this.initHandler();
|
||
$this.clearAll();
|
||
$this.handler = new Cesium.ScreenSpaceEventHandler($this.viewer.canvas);
|
||
$this.handler.setInputAction((event) => {
|
||
// 禁止地球旋转和缩放,地球的旋转会对鼠标移动监听有影响,所以需要禁止
|
||
scene.screenSpaceCameraController.enableRotate = false;
|
||
scene.screenSpaceCameraController.enableZoom = false;
|
||
scene.globe.depthTestAgainstTerrain = true;
|
||
let earthPosition = scene.pickPosition(event.position);
|
||
let pos = $this.cartesian3ToDegree(earthPosition);
|
||
$this.handler.setInputAction(function (event) {
|
||
let newPosition = scene.pickPosition(event.endPosition);
|
||
if (Cesium.defined(newPosition)) {
|
||
let pos1 = $this.cartesian3ToDegree(newPosition);
|
||
let distance = Cesium.Cartesian3.distance(newPosition, earthPosition);
|
||
let angle = $this.getAngle(pos[0], pos[1], pos1[0], pos1[1]);
|
||
let pitch = $this.getPitch(earthPosition, newPosition);
|
||
$this.ViewShedOptions = {
|
||
viewPosition: earthPosition, //观测点 笛卡尔坐标
|
||
endPosition: newPosition, //目标点 笛卡尔坐标
|
||
direction: angle, //观测方位角 默认为`0`,范围`0~360`
|
||
pitch: pitch, //俯仰角,radius,默认为`0`
|
||
horizontalViewAngle: 90, //可视域水平夹角,默认为 `90`,范围`0~360`
|
||
verticalViewAngle: 60, //可视域垂直夹角,默认为`60`,范围`0~180`
|
||
visibleAreaColor: Cesium.Color.GREEN, //可视区域颜色,默认为`green`
|
||
invisibleAreaColor: Cesium.Color.RED, //不可见区域颜色,默认为`red`
|
||
visualRange: distance, //距离,单位`米`
|
||
};
|
||
$this.updateViewShed();
|
||
}
|
||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
|
||
|
||
$this.handler.setInputAction(() => {
|
||
$this.initHandler();
|
||
// 开启地球旋转和缩放
|
||
scene.screenSpaceCameraController.enableRotate = true;
|
||
scene.screenSpaceCameraController.enableZoom = true;
|
||
$this.drawViewershed(precision);
|
||
}, Cesium.ScreenSpaceEventType.LEFT_UP);
|
||
},
|
||
|
||
ReturnDistance(pos0, pos1) {
|
||
let distance = 0;
|
||
let point1cartographic = Cesium.Cartographic.fromCartesian(pos0);
|
||
let point2cartographic = Cesium.Cartographic.fromCartesian(pos1);
|
||
/**根据经纬度计算出距离**/
|
||
let geodesic = new Cesium.EllipsoidGeodesic();
|
||
geodesic.setEndPoints(point1cartographic, point2cartographic);
|
||
let s = geodesic.surfaceDistance;
|
||
return s;
|
||
},
|
||
getHeight(x, y, objectsToExclude) {
|
||
let endCartographic = Cesium.Cartographic.fromDegrees(x, y);
|
||
let endHeight = this.viewer.scene.sampleHeight(
|
||
endCartographic,
|
||
objectsToExclude
|
||
);
|
||
return endHeight;
|
||
},
|
||
|
||
cartesian3ToDegree: function (Cartesian3) {
|
||
let _ellipsoid = this.viewer.scene.globe.ellipsoid;
|
||
let _cartographic = _ellipsoid.cartesianToCartographic(Cartesian3);
|
||
let _lat = Cesium.Math.toDegrees(_cartographic.latitude);
|
||
let _lng = Cesium.Math.toDegrees(_cartographic.longitude);
|
||
let _alt = _cartographic.height;
|
||
return [_lng, _lat, _alt];
|
||
},
|
||
getAngle: function (lng1, lat1, lng2, lat2) {
|
||
let dRotateAngle = Math.atan2(Math.abs(lng1 - lng2), Math.abs(lat1 - lat2));
|
||
if (lng2 >= lng1) {
|
||
dRotateAngle = lat2 < lat1 ? Math.PI - dRotateAngle : dRotateAngle;
|
||
} else {
|
||
dRotateAngle =
|
||
lat2 >= lat1 ? 2 * Math.PI - dRotateAngle : Math.PI + dRotateAngle;
|
||
}
|
||
dRotateAngle = (dRotateAngle * 180) / Math.PI;
|
||
return dRotateAngle;
|
||
},
|
||
|
||
getPitch(pointA, pointB) {
|
||
let transfrom = Cesium.Transforms.eastNorthUpToFixedFrame(pointA);
|
||
const vector = Cesium.Cartesian3.subtract(
|
||
pointB,
|
||
pointA,
|
||
new Cesium.Cartesian3()
|
||
);
|
||
let direction = Cesium.Matrix4.multiplyByPointAsVector(
|
||
Cesium.Matrix4.inverse(transfrom, transfrom),
|
||
vector,
|
||
vector
|
||
);
|
||
Cesium.Cartesian3.normalize(direction, direction);
|
||
return Cesium.Math.PI_OVER_TWO - Cesium.Math.acosClamped(direction.z);
|
||
},
|
||
|
||
updateViewShed: function () {
|
||
this.clear();
|
||
this.setLightCamera();
|
||
this.addVisualPyramid();
|
||
this.createFrustum();
|
||
},
|
||
clear: function () {
|
||
if (this.pyramid) {
|
||
this.viewer.entities.removeById(this.pyramid.id);
|
||
this.pyramid = undefined;
|
||
}
|
||
if (this.frustumPrimitive) {
|
||
this.viewer.scene.primitives.remove(this.frustumPrimitive);
|
||
this.frustumPrimitive = undefined;
|
||
}
|
||
if (this.debugModelMatrixPrimitive) {
|
||
this.viewer.scene.primitives.remove(this.debugModelMatrixPrimitive);
|
||
this.debugModelMatrixPrimitive = undefined;
|
||
}
|
||
},
|
||
clearAll: function () {
|
||
this.clear();
|
||
if (this.viewershedPolygon) {
|
||
this.viewer.scene.primitives.remove(this.viewershedPolygon);
|
||
this.viewershedPolygon = undefined;
|
||
}
|
||
},
|
||
addVisualPyramid: function () {
|
||
let options = this.ViewShedOptions;
|
||
let position = options.viewPosition;
|
||
let visualRange = Number(options.visualRange);
|
||
let transform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
|
||
this.debugModelMatrixPrimitive = this.viewer.scene.primitives.add(
|
||
new Cesium.DebugModelMatrixPrimitive({
|
||
modelMatrix: transform,
|
||
length: 5.0,
|
||
})
|
||
);
|
||
const halfClock = options.horizontalViewAngle / 2;
|
||
const halfCone = options.verticalViewAngle / 2;
|
||
const pitch = Cesium.Math.toDegrees(options.pitch);
|
||
const ellipsoid = new Cesium.EllipsoidGraphics({
|
||
radii: new Cesium.Cartesian3(visualRange, visualRange, visualRange),
|
||
minimumClock: Cesium.Math.toRadians(90 - options.direction - halfClock),
|
||
maximumClock: Cesium.Math.toRadians(90 - options.direction + halfClock),
|
||
minimumCone: Cesium.Math.toRadians(90 - pitch - halfCone),
|
||
maximumCone: Cesium.Math.toRadians(90 - pitch + halfCone),
|
||
fill: false,
|
||
outline: true,
|
||
subdivisions: 256,
|
||
stackPartitions: 64,
|
||
slicePartitions: 64,
|
||
outlineColor: Cesium.Color.YELLOWGREEN.withAlpha(0.5),
|
||
});
|
||
const pyramidEntity = new Cesium.Entity({
|
||
position: position,
|
||
ellipsoid,
|
||
});
|
||
this.pyramid = this.viewer.entities.add(pyramidEntity);
|
||
},
|
||
setLightCamera: function () {
|
||
if (!this.lightCamera) {
|
||
this.lightCamera = new Cesium.Camera(this.viewer.scene);
|
||
}
|
||
let options = this.ViewShedOptions;
|
||
let visualRange = Number(options.visualRange);
|
||
this.lightCamera.position = options.viewPosition;
|
||
this.lightCamera.frustum.near = 0.1;
|
||
this.lightCamera.frustum.far = visualRange;
|
||
const hr = Cesium.Math.toRadians(options.horizontalViewAngle);
|
||
const vr = Cesium.Math.toRadians(options.verticalViewAngle);
|
||
this.lightCamera.frustum.aspectRatio =
|
||
(visualRange * Math.tan(hr / 2) * 2) /
|
||
(visualRange * Math.tan(vr / 2) * 2);
|
||
this.lightCamera.frustum.fov = hr > vr ? hr : vr;
|
||
this.lightCamera.setView({
|
||
destination: options.viewPosition,
|
||
orientation: {
|
||
heading: Cesium.Math.toRadians(options.direction || 0),
|
||
pitch: options.pitch || 0,
|
||
roll: 0,
|
||
},
|
||
});
|
||
},
|
||
createFrustum: function () {
|
||
const scratchRight = new Cesium.Cartesian3();
|
||
const scratchRotation = new Cesium.Matrix3();
|
||
const scratchOrientation = new Cesium.Quaternion();
|
||
const direction = this.lightCamera.directionWC;
|
||
const up = this.lightCamera.upWC;
|
||
let right = this.lightCamera.rightWC;
|
||
right = Cesium.Cartesian3.negate(right, scratchRight);
|
||
let rotation = scratchRotation;
|
||
Cesium.Matrix3.setColumn(rotation, 0, right, rotation);
|
||
Cesium.Matrix3.setColumn(rotation, 1, up, rotation);
|
||
Cesium.Matrix3.setColumn(rotation, 2, direction, rotation);
|
||
let orientation = Cesium.Quaternion.fromRotationMatrix(
|
||
rotation,
|
||
scratchOrientation
|
||
);
|
||
let instanceOutline = new Cesium.GeometryInstance({
|
||
geometry: new Cesium.FrustumOutlineGeometry({
|
||
frustum: this.lightCamera.frustum,
|
||
origin: this.ViewShedOptions.viewPosition,
|
||
orientation: orientation,
|
||
}),
|
||
id: "视椎体轮廓线" + Math.random().toString(36).substr(2),
|
||
attributes: {
|
||
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
||
new Cesium.Color(0.0, 1.0, 0.0, 1.0)
|
||
),
|
||
show: new Cesium.ShowGeometryInstanceAttribute(true),
|
||
},
|
||
});
|
||
this.frustumPrimitive = this.viewer.scene.primitives.add(
|
||
new Cesium.Primitive({
|
||
geometryInstances: instanceOutline,
|
||
appearance: new Cesium.PerInstanceColorAppearance({
|
||
flat: true,
|
||
translucent: false,
|
||
closed: true,
|
||
}),
|
||
})
|
||
);
|
||
},
|
||
createPoint: function (firstPos, secondPos) {
|
||
let entity4FirstPos = new Cesium.Entity({
|
||
name: "firstPos",
|
||
show: true,
|
||
position: firstPos,
|
||
point: {
|
||
show: true,
|
||
pixelSize: 20,
|
||
color: Cesium.Color.RED,
|
||
outlineColor: Cesium.Color.YELLOW,
|
||
outlineWidth: 5,
|
||
},
|
||
description: `
|
||
<p>这是绘制的视椎体起点</p>`,
|
||
});
|
||
this.viewer.entities.add(entity4FirstPos);
|
||
let entity4SecondPos = new Cesium.Entity({
|
||
name: "secondPos",
|
||
show: true,
|
||
position: secondPos,
|
||
point: {
|
||
show: true,
|
||
pixelSize: 30,
|
||
color: Cesium.Color.YELLOW,
|
||
outlineColor: Cesium.Color.RED,
|
||
outlineWidth: 8,
|
||
},
|
||
description: `
|
||
<p>这是绘制的视椎体视角终点</p>`,
|
||
});
|
||
this.viewer.entities.add(entity4SecondPos);
|
||
},
|
||
|
||
//绘制可视域
|
||
add(positionArr) {
|
||
let polygon = new Cesium.PolygonGeometry({
|
||
polygonHierarchy: new Cesium.PolygonHierarchy(
|
||
Cesium.Cartesian3.fromDegreesArray(positionArr)
|
||
),
|
||
height: 0.0,
|
||
extrudedHeight: 0.0,
|
||
vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
|
||
stRotation: 0.0, // 纹理的旋转坐标(以弧度为单位),正旋转是逆时针方向
|
||
ellipsoid: Cesium.Ellipsoid.WGS84,
|
||
granularity: Cesium.Math.RADIANS_PER_DEGREE, // 每个纬度和经度之间的距离(以弧度为单位),确定缓冲区中的位置数
|
||
perPositionHeight: false, // 每个位置点使用的高度
|
||
closeTop: true,
|
||
closeBottom: true,
|
||
// NONE 与椭圆表面不符的直线;GEODESIC 遵循测地路径;RHUMB 遵循大黄蜂或恶魔般的道路。
|
||
arcType: Cesium.ArcType.GEODESIC, // 多边形边缘线型
|
||
});
|
||
|
||
let polygonInstance = new Cesium.GeometryInstance({
|
||
geometry: polygon,
|
||
name: "ViewershedPolygon",
|
||
attributes: {
|
||
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
||
Cesium.Color.BLUE.withAlpha(0.6)
|
||
),
|
||
show: new Cesium.ShowGeometryInstanceAttribute(true), //显示或者隐藏
|
||
},
|
||
});
|
||
this.viewershedPolygon = this.viewer.scene.primitives.add(
|
||
new Cesium.GroundPrimitive({
|
||
geometryInstances: polygonInstance,
|
||
appearance: new Cesium.EllipsoidSurfaceAppearance({
|
||
aboveGround: true,
|
||
material: new Cesium.Material({
|
||
fabric: {
|
||
type: "Image",
|
||
uniforms: {
|
||
image: this.returnImgae(),
|
||
},
|
||
},
|
||
}),
|
||
}),
|
||
})
|
||
);
|
||
},
|
||
drawViewershed(precision) {
|
||
const pos = this.cartesian3ToDegree(this.ViewShedOptions.viewPosition);
|
||
const radius = this.ViewShedOptions.visualRange;
|
||
const direction = this.ViewShedOptions.direction;
|
||
let boundary = this.computeBoundaryOptions(pos, radius, direction);
|
||
const bbox = boundary.bbox;
|
||
let mask = turf.polygon([boundary.boundaryPoints]);
|
||
const dis = this.ViewShedOptions.visualRange / (precision * 1000);
|
||
let gridPoints = turf.pointGrid(bbox, dis, { mask: mask });
|
||
|
||
let pointsResult = this.createTargetPoints(gridPoints, dis, pos);
|
||
let variogram = kriging.train(
|
||
pointsResult.values,
|
||
pointsResult.lngs,
|
||
pointsResult.lats,
|
||
"exponential",
|
||
0,
|
||
100
|
||
);
|
||
let grid = kriging.grid([boundary.boundaryPoints], variogram, dis / 1000);
|
||
const colors = [
|
||
"#ff000080",
|
||
"#ff000080",
|
||
"#ff000080",
|
||
"#ff000080",
|
||
"#ff000080",
|
||
"#ff000080",
|
||
"#00ff0080",
|
||
"#00ff0080",
|
||
"#00ff0080",
|
||
"#00ff0080",
|
||
"#00ff0080",
|
||
"#00ff0080",
|
||
];
|
||
|
||
this.canvasEle.width = 3840;
|
||
this.canvasEle.height = 2160;
|
||
kriging.plot(
|
||
this.canvasEle,
|
||
grid,
|
||
[bbox[0], bbox[2]],
|
||
[bbox[1], bbox[3]],
|
||
colors
|
||
);
|
||
this.add(boundary.positionArr);
|
||
},
|
||
computeBoundaryOptions(pos, radius, angle) {
|
||
let Ea = 6378137; // 赤道半径
|
||
let Eb = 6356725; // 极半径
|
||
const lng = pos[0],
|
||
lat = pos[1];
|
||
const bbox = [lng, lat, lng, lat]; //[minX, minY, maxX, maxY]
|
||
let positionArr = [];
|
||
let boundaryPoints = [];
|
||
positionArr.push(lng, lat);
|
||
boundaryPoints.push([lng, lat]);
|
||
//正北是0°
|
||
let start = angle + 45 > 360 ? angle - 45 - 360 : angle - 45;
|
||
let end = start + 90;
|
||
for (let i = start; i <= end; i++) {
|
||
let dx = radius * Math.sin((i * Math.PI) / 180.0);
|
||
let dy = radius * Math.cos((i * Math.PI) / 180.0);
|
||
let ec = Eb + ((Ea - Eb) * (90.0 - lat)) / 90.0;
|
||
let ed = ec * Math.cos((lat * Math.PI) / 180);
|
||
let BJD = lng + ((dx / ed) * 180.0) / Math.PI;
|
||
let BWD = lat + ((dy / ec) * 180.0) / Math.PI;
|
||
positionArr.push(BJD, BWD);
|
||
boundaryPoints.push([BJD, BWD]);
|
||
this.refreshBBox(bbox, BJD, BWD);
|
||
}
|
||
boundaryPoints.push([lng, lat]);
|
||
return {
|
||
positionArr,
|
||
boundaryPoints,
|
||
bbox,
|
||
};
|
||
},
|
||
/**
|
||
* 更新外围矩形 Bbox
|
||
* @param {Array} result 外围矩形Bbox-[minX, minY, maxX, maxY]
|
||
* @param {Number} x 经度
|
||
* @param {Number} y 纬度
|
||
*/
|
||
refreshBBox(result, x, y) {
|
||
result[0] = x < result[0] ? x : result[0];
|
||
result[1] = y < result[1] ? y : result[1];
|
||
result[2] = x > result[2] ? x : result[2];
|
||
result[3] = y > result[3] ? y : result[3];
|
||
},
|
||
/**
|
||
* 插值点用射线判断通视性
|
||
* @param {*} gridPoints 网格点
|
||
* @param {*} step 步长,可以理解成是精度
|
||
* @param {*} sourcePos 视域分析起点
|
||
* @returns kriging插值所需的参数对象{ values:[], lngs:[], lats:[]}
|
||
*/
|
||
createTargetPoints(gridPoints, step, sourcePos) {
|
||
let positionArr = [];
|
||
let objectsToExclude = [
|
||
this.frustumPrimitive,
|
||
this.pyramid,
|
||
this.debugModelMatrixPrimitive,
|
||
];
|
||
let values = [],
|
||
lngs = [],
|
||
lats = [];
|
||
let height = this.getHeight(sourcePos[0], sourcePos[1], objectsToExclude);
|
||
positionArr.push({
|
||
x: sourcePos[0],
|
||
y: sourcePos[1],
|
||
z: height,
|
||
});
|
||
let viewPoint = this.ViewShedOptions.viewPosition;
|
||
for (let index = 0; index < gridPoints.features.length; index++) {
|
||
const feature = gridPoints.features[index];
|
||
const coords = feature.geometry.coordinates;
|
||
const x = coords[0],
|
||
y = coords[1];
|
||
let h = this.getHeight(x, y, objectsToExclude);
|
||
let endPoint = Cesium.Cartesian3.fromDegrees(x, y, h);
|
||
let direction = Cesium.Cartesian3.normalize(
|
||
Cesium.Cartesian3.subtract(
|
||
endPoint,
|
||
viewPoint,
|
||
new Cesium.Cartesian3()
|
||
),
|
||
new Cesium.Cartesian3()
|
||
);
|
||
// 建立射线
|
||
let ray = new Cesium.Ray(viewPoint, direction);
|
||
let result = this.viewer.scene.pickFromRay(ray, objectsToExclude); // 计算交互点,返回第一个
|
||
if (result) {
|
||
let buffer = this.ReturnDistance(endPoint, result.position);
|
||
// let M_color = Cesium.Color.GREEN;
|
||
if (buffer > step) {
|
||
// M_color = Cesium.Color.RED;
|
||
values.push(0);
|
||
} else {
|
||
values.push(1);
|
||
}
|
||
lngs.push(x);
|
||
lats.push(y);
|
||
// this.viewer.entities.add(
|
||
// new Cesium.Entity({
|
||
// name: "插值点哦",
|
||
// show: true,
|
||
// position: endPoint,
|
||
// point: {
|
||
// show: true,
|
||
// pixelSize: 10,
|
||
// color: M_color,
|
||
// outlineWidth: 2,
|
||
// outlineColor: Cesium.Color.YELLOW,
|
||
// },
|
||
// })
|
||
// );
|
||
}
|
||
}
|
||
return {
|
||
values,
|
||
lngs,
|
||
lats,
|
||
};
|
||
},
|
||
/**
|
||
* canvas转image图片
|
||
* @returns base64图片
|
||
*/
|
||
returnImgae() {
|
||
return this.canvasEle.toDataURL("image/png");
|
||
},
|
||
};
|
||
|
||
export default ViewShed; |