Skip to content

Commit 35e5063

Browse files
mapboxgl 视频图层支持加载非定点数据
1 parent 592a544 commit 35e5063

File tree

9 files changed

+687
-124
lines changed

9 files changed

+687
-124
lines changed

examples/locales/en-US/resources.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ window.examplesResources = {
738738
"title_dataAttributes": "Attributes",
739739
"title_videoLayer":"Video Layer",
740740
"title_videoMap":"Video Map",
741+
"title_uavVideo":"Uav Video",
741742
"title_knowledgeGraphMap": "KnowledgeGraph",
742743
"title_l7_grid_map": "Grid Map",
743744
"title_l7_circular_sweeping_city": "Circular Sweeping City",

examples/locales/zh-CN/resources.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ window.examplesResources = {
713713
"text_selectEndNode": "捕捉结束节点",
714714
"title_videoLayer":"视频图层",
715715
"title_videoMap":"视频地图",
716+
"title_uavVideo":"无人机视频",
716717
"title_knowledgeGraphMap": "知识图谱",
717718
"title_l7_grid_map": "网格地图",
718719
"title_l7_circular_sweeping_city": "圆形扫光城市",

examples/mapboxgl/config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,6 +1875,13 @@ var exampleConfig = {
18751875
version: '11.2.0',
18761876
thumbnail: 'videoMap.png',
18771877
fileName: 'videoMap'
1878+
},
1879+
{
1880+
name: '无人机视频',
1881+
name_en: 'UAV video',
1882+
version: '11.2.0',
1883+
thumbnail: 'videoLayerWithTime.png',
1884+
fileName: 'videoLayerWithTime'
18781885
}
18791886
]
18801887
}
121 KB
Loading
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!--********************************************************************
2+
* Copyright© 2000 - 2025 SuperMap Software Co.Ltd. All rights reserved.
3+
*********************************************************************-->
4+
<!--********************************************************************
5+
* 该示例需要引入
6+
* mapbox-gl-enhance (https://iclient.supermap.io/web/libs/mapbox-gl-js-enhance/1.12.1-10/mapbox-gl-enhance.js)
7+
* Turf (https://github.com/Turfjs/turf/)
8+
* proj4js (https://github.com/proj4js/proj4js)
9+
* video.js (https://github.com/videojs/video.js)
10+
* opencv (https://github.com/opencv/opencv)
11+
*********************************************************************-->
12+
<!DOCTYPE html>
13+
<html>
14+
15+
<head>
16+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
17+
<title data-i18n="resources.title_uavVideo"></title>
18+
<style type="text/css">
19+
body {
20+
margin: 0;
21+
padding: 0;
22+
}
23+
24+
#map {
25+
position: absolute;
26+
top: 0;
27+
bottom: 0;
28+
width: 100%;
29+
}
30+
</style>
31+
</head>
32+
33+
<body>
34+
<div id="map"></div>
35+
<script type="text/javascript" include="bootstrap" src="../js/include-web.js"></script>
36+
<script type="text/javascript" include="mapbox-gl-enhance,proj4,turf,videojs,opencv"
37+
src="../../dist/mapboxgl/include-mapboxgl.js"></script>
38+
<script type="text/javascript">
39+
var map, videoLayer;
40+
var attribution =
41+
"<a href='https://www.mapbox.com/about/maps/' target='_blank'>© Mapbox </a>" +
42+
" with <span>© <a href='https://iclient.supermap.io' target='_blank'>SuperMap iClient</a> | </span>" +
43+
" Map Data <span>© <a href='http://support.supermap.com.cn/product/iServer.aspx' target='_blank'>SuperMap iServer</a></span> ";
44+
var dataUrl = 'https://iserver.supermap.io/iserver/services/data-video/rest/data';
45+
var datasourceName = 'VideoData';
46+
var datasetName = 'DJI_09304';
47+
var map = new mapboxgl.Map({
48+
container: 'map',
49+
renderWorldCopies: false,
50+
style: {
51+
version: 8,
52+
sources: {
53+
'raster-tiles': {
54+
type: 'raster',
55+
tileSize: 256,
56+
tiles: [
57+
'https://t4.tianditu.gov.cn/img_w/wmts?service=WMTS&request=GetTile&version=1.0.0&style=default&tilematrixSet=w&format=tiles&width=256&height=256&layer=img&tilematrix={z}&tilerow={y}&tilecol={x}&tk=1d109683f4d84198e37a38c442d68311'
58+
]
59+
}
60+
},
61+
layers: [
62+
{
63+
id: 'simple-tiles',
64+
type: 'raster',
65+
source: 'raster-tiles',
66+
minzoom: 0,
67+
maxzoom: 22
68+
}
69+
]
70+
},
71+
center: [104.0685393316582, 30.491810619411483],
72+
zoom: 17
73+
});
74+
map.addControl(new mapboxgl.NavigationControl(), 'top-left');
75+
map.addControl(new mapboxgl.supermap.LogoControl({ link: 'https://iclient.supermap.io' }), 'bottom-right');
76+
function query() {
77+
var sqlParam = new mapboxgl.supermap.GetFeaturesBySQLParameters({
78+
queryParameter: {
79+
name: datasetName + '@' + datasourceName,
80+
attributeFilter: 'SMID > 0'
81+
},
82+
datasetNames: [datasourceName + ':' + datasetName]
83+
});
84+
85+
new mapboxgl.supermap.FeatureService(dataUrl).getFeaturesBySQL(sqlParam).then(function (serviceResult) {
86+
const timeData = [];
87+
serviceResult.result.features.features[0].properties.videoParameters.videoParameterList.forEach((param) => {
88+
let coordinates1 = [];
89+
param.extent.points.forEach((coord) => {
90+
const res = proj4('EPSG:3857', 'EPSG:4326', coord);
91+
coordinates1.push(res);
92+
});
93+
let cameraLocation = param.calibrationModel.cameraLocation;
94+
let calibrationModel = param.calibrationModel;
95+
timeData.push({
96+
time: param.time,
97+
extent: [coordinates1[3], coordinates1[2], coordinates1[1], coordinates1[0]],
98+
fovX: calibrationModel.fovX,
99+
fovY: calibrationModel.fovY,
100+
centerX: calibrationModel.centerX,
101+
centerY: calibrationModel.centerY,
102+
pitch: cameraLocation.cameraPitch,
103+
roll: cameraLocation.cameraRoll,
104+
yaw: cameraLocation.cameraYaw,
105+
x: cameraLocation.cameraX,
106+
y: cameraLocation.cameraY,
107+
z: cameraLocation.cameraZ
108+
});
109+
});
110+
let coordinates = [];
111+
turf.featureEach(serviceResult.result.features, function (currentFeature, featureIndex) {
112+
currentFeature.geometry.coordinates[0][0].forEach((coord) => {
113+
const res = proj4('EPSG:3857', 'EPSG:4326', coord);
114+
coordinates.push(res);
115+
});
116+
});
117+
var url = serviceResult.result.features.features[0].properties.address;
118+
var cameraLocation = serviceResult.result.features.features[0].properties.cameraLocation;
119+
var calibrationModel =
120+
serviceResult.result.features.features[0].properties.videoParameters.videoParameterList[0].calibrationModel;
121+
var clipRegionPoints =
122+
serviceResult.result.features.features[0].properties.videoParameters.videoParameterList[0].clipRegion
123+
.points;
124+
videoLayer = new mapboxgl.supermap.VideoLayer({
125+
url,
126+
videoParameters: timeData,
127+
clipRegion: [
128+
[clipRegionPoints[0].x, clipRegionPoints[0].y],
129+
[clipRegionPoints[1].x, clipRegionPoints[1].y],
130+
[clipRegionPoints[2].x, clipRegionPoints[2].y],
131+
[clipRegionPoints[3].x, clipRegionPoints[3].y]
132+
]
133+
});
134+
map.addLayer(videoLayer);
135+
});
136+
}
137+
map.on('load', function () {
138+
query();
139+
});
140+
</script>
141+
</body>
142+
143+
</html>

src/common/overlay/video/VideoLayerRenderer.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ export class VideoLayerRenderer {
5757
muted: true,
5858
loop: this.loop
5959
};
60-
if (this.url.includes('mp4')) {
60+
if (this.url.includes('mp4') || this.url.includes('MP4')) {
6161
options['sources'] = [
6262
{
6363
src: this.url,
6464
type: 'video/mp4'
6565
}
6666
];
67-
} else if (this.url.includes('flv')) {
67+
} else if (this.url.includes('flv') || this.url.includes('FLV')) {
6868
options = {
6969
loop: this.loop,
7070
autoplay: this.autoplay,
@@ -84,7 +84,7 @@ export class VideoLayerRenderer {
8484
}
8585
]
8686
};
87-
} else if (this.url.includes('m3u8')) {
87+
} else if (this.url.includes('m3u8') || this.url.includes('M3U8')) {
8888
options['sources'] = [
8989
{
9090
src: this.url

src/mapboxgl/mapping/utils/VideoMapUtil.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,133 @@ export const fovXToFx = (fovX, videoWidth) => {
2828

2929
export const fovYToFy = (fovY, videoHeight) => {
3030
return videoHeight / (2 * Math.tan(fovY / 2 * Math.PI / 180));
31+
}
32+
33+
/**
34+
* @private
35+
*/
36+
37+
export class FastRangeSearcher {
38+
constructor(sortedArray) {
39+
this.data = sortedArray.map(n => Math.round(n * 1000));
40+
}
41+
42+
findNearest(target) {
43+
const targetInt = Math.round(target * 1000);
44+
const minVal = targetInt;
45+
const maxVal = targetInt + 3000;
46+
47+
let closestIndex = -1;
48+
let minDiff = Infinity;
49+
50+
for (let i = 0; i < this.data.length; i++) {
51+
const current = this.data[i];
52+
53+
if (current > maxVal) {
54+
break
55+
}
56+
57+
if (current >= minVal) {
58+
const diff = current - targetInt;
59+
if (diff < minDiff) {
60+
minDiff = diff;
61+
closestIndex = i;
62+
}
63+
}
64+
}
65+
66+
if (closestIndex !== -1) {
67+
return {
68+
value: +(this.data[closestIndex] / 1000).toFixed(3)
69+
};
70+
}
71+
72+
return null;
73+
}
74+
}
75+
76+
/**
77+
* @private
78+
* @param {Object} params 配置参数
79+
* @param {number} params.interval 目标时间间隔(秒,≥0.001)
80+
* @param {Array} params.data 时间序列数据
81+
* @returns {Array} 处理结果
82+
*/
83+
export function smartTimeProcessor(interval, data, properties = []) {
84+
if (interval < 0.001) {
85+
throw new Error("time interval can't' less than 0.001");
86+
}
87+
if (!data || data.length === 0) {
88+
return [];
89+
}
90+
91+
const processed = data.map(p => ({
92+
...p,
93+
time: Math.round(p.time * 1000)
94+
}));
95+
96+
// 计算最小原始间隔和有效时间范围
97+
let minInterval = Infinity;
98+
for (let i = 1; i < processed.length; i++) {
99+
minInterval = Math.min(minInterval, processed[i].time - processed[i-1].time);
100+
}
101+
const targetInterval = Math.round(interval * 1000);
102+
const [startMs, endMs] = [
103+
processed[0].time,
104+
processed[processed.length - 1].time
105+
];
106+
107+
if (targetInterval === minInterval) {
108+
return data;
109+
}
110+
111+
const useInterpolation = targetInterval < minInterval;
112+
113+
let result = [];
114+
let dataPtr = 0;
115+
116+
for (let t = startMs; t <= endMs; t += targetInterval) {
117+
while (dataPtr < processed.length - 1 && processed[dataPtr + 1].time < t) {
118+
dataPtr++;
119+
}
120+
121+
const current = processed[dataPtr];
122+
const next = processed[dataPtr + 1] || current;
123+
124+
if (useInterpolation) {
125+
const ratio = next.time === current.time ? 0 : (t - current.time) / (next.time - current.time);
126+
let res = {};
127+
properties.forEach(prop => {
128+
if (prop === 'extent') {
129+
res[prop] = current[prop].map((item, index) => {
130+
return [current[prop][index].x + (next[prop][index].x - current[prop][index].x) * ratio, current[prop][index].y + (next[prop][index].y - current[prop][index].y) * ratio];
131+
});
132+
} else {
133+
const value = current[prop] + (next[prop] - current[prop]) * ratio;
134+
res[prop] = +value;
135+
}
136+
});
137+
result.push({
138+
...current,
139+
time: +((t / 1000).toFixed(3)),
140+
...res
141+
});
142+
} else {
143+
if (current.time === t) {
144+
result.push(formatPoint(current));
145+
dataPtr++;
146+
} else if (next.time === t) {
147+
result.push(formatPoint(next));
148+
dataPtr++;
149+
}
150+
}
151+
}
152+
return result;
153+
}
154+
155+
function formatPoint(point) {
156+
return {
157+
...point,
158+
time: +(point.time / 1000).toFixed(3)
159+
};
31160
}

0 commit comments

Comments
 (0)