diff --git a/src/mapboxgl/overlay/VideoLayer.js b/src/mapboxgl/overlay/VideoLayer.js index 95a96c9a5..f945d8045 100644 --- a/src/mapboxgl/overlay/VideoLayer.js +++ b/src/mapboxgl/overlay/VideoLayer.js @@ -1,167 +1,254 @@ /* Copyright© 2000 - 2024 SuperMap Software Co.Ltd. All rights reserved. * This program are made available under the terms of the Apache License, Version 2.0 * which accompanies this distribution and is available at http://www.apache.org/licenses/LICENSE-2.0.html.*/ -import mapboxgl from 'mapbox-gl'; -import { Util as CommonUtil } from '@supermap/iclient-common/commontypes/Util'; -import { VideoLayerRenderer } from '@supermap/iclient-common/overlay/video/VideoLayerRenderer'; -import { bbox, polygon } from '@turf/turf'; - -/** - * @class VideoLayer - * @category Visualization Video - * @classdesc 视频图层,用于将配准后的视频、视频流(HLS(m3u8)、HTTP-FLV)叠加至地图上。 - * @modulecategory Overlay - * @version 11.2.0 - * @param {Object} options - 构造参数。 - * @param {string} options.url - 视频 或 流链接。支持 flv, m3u8, map4 格式。 - * @param {Array} options.extent - 视频范围。 - * @param {Object} [options.opencv] - opencv.js 实例, 未传入时将去 window.cv 获取。 - * @param {string} [options.id] - 视频图层 ID。默认使用 CommonUtil.createUniqueID("VideoLayer_") 创建专题图层 ID。 - * @extends {mapboxgl.Evented} - * @usage - */ -export class VideoLayer extends mapboxgl.Evented { - - constructor(options) { - super(); - var _options = options ? options : {}; - this.options = _options; - this.url = this.options.url; - this.extent = this.options.extent; - this.cv = this.options.opencv || window.cv; - if (!this.cv) { - throw new Error('opencv.js instance is not existed!'); - } - this.id = _options.id ? _options.id : CommonUtil.createUniqueID("VideoLayer_"); - this.layerId = this.id + '_outer'; - this.type = 'custom'; - this.renderingMode = '3d'; - this.overlay = true; - } - - /** - * @function VideoLayer.prototype.onAdd - * @description 添加该图层。 - */ - onAdd(map) { - this.map = map; - this.renderer = new VideoLayerRenderer({ url: this.url, id: this.layerId }); - this.video = this.renderer.createVideo(); - this.videoDomId = this.renderer.getVideoDomId(); - this.video.one('firstplay', () => { - this.video.play(); - }); - this.video.one('ready', () => { - setTimeout(() => { - this.videoWidth = this.video.videoWidth(); - this.videoHeight = this.video.videoHeight(); - this._addVideoLayer(this.map); - }, 1000); - }); - this.video.one('canplay', () => { - setTimeout(() => { - map.getSource(this.layerId).play(); - }, 1500); - }); - } - - render() {} - - _getPixelBbox(map) { - let res = []; - let minX = 0; - let minY = 0; - this.extent.forEach((item) => { - let result = map.project(item); - if (!minX || result.x < minX) { - minX = result.x; - } - if (!minY || result.y < minY) { - minY = result.y; - } - res.push(result.x); - res.push(result.y); - }); - res = res.map((item, index) => { - if (index % 2 === 0) { - return item - minX; - } else { - return item - minY; - } - }); - return res; - } - - _addVideoLayer(map) { - let url = this.videoDomId || this.url; - this.pixelBBox = this._getPixelBbox(map); - const result = bbox(polygon([ - this.extent.concat(this.extent[0]) - ])); - let br = map.project([result[2], result[3]]); - let tl = map.project([result[0], result[1]]); - let size = [Math.abs(br.x - tl.x), Math.abs(br.y - tl.y)]; - let ratio = size[0] / size[1]; - let realX = this.videoHeight * ratio; - let realY = this.videoHeight; - let ratioX = realX / size[0]; - let ratioY = realY / size[1]; - this.pixelBBox = this.pixelBBox.map((item, index) => { - if (index % 2 === 0) { - return item * ratioX; - } else { - return item * ratioY; - } - }); - this.dsize = new this.cv.Size(this.videoHeight * ratio, this.videoHeight); - let that = this; - let srcTri = this.cv.matFromArray(4, 1, this.cv.CV_32FC2, [0, 0, that.videoWidth, 0, that.videoWidth, that.videoHeight, 0, that.videoHeight]); - let dstTri = this.cv.matFromArray(4, 1, this.cv.CV_32FC2, this.pixelBBox); - map.addSource(this.layerId, { - type: 'video', - urls: [url], - drawImageCallback(frame) { - let src = that.cv.matFromImageData(frame); - let dst = new that.cv.Mat(); - let M = that.cv.findHomography(srcTri, dstTri); - that.cv.warpPerspective(src, dst, M, that.dsize); - let newFrame = new ImageData(new Uint8ClampedArray(dst.data), dst.cols, dst.rows); - src.delete(); - dst.delete(); - return newFrame; - }, - coordinates: [ - [result[0], result[3]], - [result[2], result[3]], - [result[2], result[1]], - [result[0], result[1]] - ] - }); - - map.addLayer( - { - id: this.layerId, - type: 'raster', - source: this.layerId - } - ); - } - - /** - * @function VideoLayer.prototype.moveLayer - * @description 移动图层。 + import mapboxgl from 'mapbox-gl'; + import { Util as CommonUtil } from '@supermap/iclient-common/commontypes/Util'; + import { VideoLayerRenderer } from '@supermap/iclient-common/overlay/video/VideoLayerRenderer'; + import { bbox, polygon } from '@turf/turf'; + import CoordTransfer from '../mapping/CoordTransfer'; + import { fovXToFx, fovYToFy } from '../mapping/util'; + import proj4 from 'proj4'; + + /** + * @class VideoLayer + * @category Visualization Video + * @classdesc 视频图层,用于将配准后的视频、视频流(HLS(m3u8)、HTTP-FLV)叠加至地图上。 + * @modulecategory Overlay + * @version 11.2.0 + * @param {Object} options - 构造参数。 + * @param {string} options.url - 视频 或 流链接。支持 flv, m3u8, map4 格式。 + * @param {Object} options.videoParameters - 视频配准参数 + * @param {Array} options.extent - 视频范围。 + * @param {Object} [options.opencv] - opencv.js 实例, 未传入时将去 window.cv 获取。 + * @param {string} [options.id] - 视频图层 ID。默认使用 CommonUtil.createUniqueID("VideoLayer_") 创建专题图层 ID。 + * @param {Array} [options.clipRegion] -裁剪范围 + * @extends {mapboxgl.Evented} + * @usage + */ + export class VideoLayer extends mapboxgl.Evented { + + constructor(options) { + super(); + var _options = options ? options : {}; + this.options = _options; + this.url = this.options.url; + this.extent = this.options.extent; + this.clipRegion = this.options.clipRegion || []; + this.videoParameters = this.options.videoParameters; + this.cv = this.options.opencv || window.cv; + if (!this.cv) { + throw new Error('opencv.js instance is not existed!'); + } + if (!this.videoParameters) { + throw new Error('videoParameters must be set!'); + } + + this.id = _options.id ? _options.id : CommonUtil.createUniqueID("VideoLayer_"); + this.layerId = this.id + '_outer'; + this.type = 'custom'; + this.renderingMode = '3d'; + this.overlay = true; + } + + /** + * @function VideoLayer.prototype.onAdd + * @description 添加该图层。 */ - moveLayer(beforeId) { - this.map.moveLayer(this.layerId, beforeId); - } - - /** - * @function VideoLayer.prototype.setVisibility - * @description 设置图层可见性。 - * @param {boolean} [visibility] - 是否显示图层。 - */ - setVisibility(visibility) { - const visible = visibility ? 'visible' : 'none'; - this.map.setLayoutProperty(this.layerId, 'visibility', visible); - } -} + onAdd(map) { + this.map = map; + this.renderer = new VideoLayerRenderer({ url: this.url, id: this.layerId }); + this.video = this.renderer.createVideo(); + this.videoDomId = this.renderer.getVideoDomId(); + this.video.one('firstplay', () => { + this.video.play(); + }); + this.video.one('ready', () => { + setTimeout(() => { + this.videoWidth = this.video.videoWidth(); + this.videoHeight = this.video.videoHeight(); + this._addVideoLayer(this.map); + }, 1000); + }); + this.video.one('canplay', () => { + setTimeout(() => { + map.getSource(this.layerId).play(); + }, 1500); + }); + } + + render() { } + + _getPixelBbox(bounds, bbox) { + let minX = 0; + let minY = 0; + bounds.forEach((item, index) => { + if ((!minX || item < minX) && index % 2 === 0) { + minX = item; + } + if ((!minY || item < minY) && index % 2 !== 0) { + minY = item; + } + }); + let maxX = 0; + let maxY = 0; + if (bbox) { + let pixelLeftBottom = this.map.project([bbox[0], bbox[1]]); + let pixelRightTop = this.map.project([bbox[2], bbox[3]]); + maxX = Math.min(Math.abs(minX - pixelLeftBottom.x), Math.abs(minX - pixelRightTop.x)); + maxY = Math.max(Math.abs(minY - pixelLeftBottom.y), Math.abs(minY - pixelRightTop.y)); + } + bounds = bounds.map((item, index) => { + if (index % 2 === 0) { + return item - minX - maxX; + } else { + return item - minY + maxY; + } + }); + return bounds; + } + + _initParameters(parameters) { + if (parameters && !Object.keys(parameters).length) { + return; + } + parameters.fx = fovXToFx(parameters.fovX, this.videoWidth); + parameters.fy = fovYToFy(parameters.fovY, this.videoHeight); + return new CoordTransfer(this.cv, parameters).init(); + } + + _addVideoLayer(map) { + let url = this.videoDomId || this.url; + const poly = polygon([ + [...this.extent, this.extent[0]] + ]); + const result = bbox(poly); + + let that = this; + let realHeight = this.videoHeight; + let srcPixelCoords = [0, 0, that.videoWidth, 0, that.videoWidth, that.videoHeight, 0, that.videoHeight]; + let temp = [[0, 0], [that.videoWidth, 0], [that.videoWidth, that.videoHeight], [0, that.videoHeight]]; + if (this.clipRegion.length) { + srcPixelCoords = []; + temp = this.clipRegion; + this.clipRegion.forEach((coord) => { + srcPixelCoords.push(coord[0]); + srcPixelCoords.push(coord[1]); + }); + realHeight = this.clipRegion[2][1] - this.clipRegion[0][1]; + } + let srcTri = this.cv.matFromArray(4, 1, this.cv.CV_32FC2, + srcPixelCoords + ); + + this._initParameters(this.videoParameters).then((coordTransfer) => { + this.coordTransfer = coordTransfer; + let latlng = []; + temp.forEach((point) => { + let coord = this.coordTransfer.toSpatialCoordinate(point); + let tcoord = [coord[0], coord[1]]; + const pcoord = proj4('EPSG:3857', 'EPSG:4326', tcoord); + latlng.push(pcoord); + }); + let originBounds = []; + this.extent.forEach((coord) => { + let res = map.project(coord); + originBounds.push(res.x); + originBounds.push(res.y); + }); + + let pointCoords = [] + latlng.forEach((item) => { + let result = map.project(item); + pointCoords.push(result.x); + pointCoords.push(result.y); + }); + let br = map.project([result[2], result[3]]); + let tl = map.project([result[0], result[1]]); + let size = [Math.abs(br.x - tl.x), Math.abs(br.y - tl.y)]; + let ratio = size[0] / size[1]; + this.dsize = new this.cv.Size(realHeight * ratio, realHeight); + let realX = realHeight * ratio; + let realY = realHeight; + let ratioX = realX / size[0]; + let ratioY = realY / size[1]; + let realRatio = Math.min(ratioX, ratioY); + pointCoords = this._getPixelBbox(pointCoords, result); + pointCoords = pointCoords.map((item) => { + return item * realRatio; + }); + originBounds = this._getPixelBbox(originBounds); + originBounds = originBounds.map((item) => { + return item * realRatio; + }); + let dstTri = this.cv.matFromArray(4, 1, this.cv.CV_32FC2, pointCoords); + let canvas = document.createElement('canvas'); + canvas.width = realHeight * ratio; + canvas.height = realHeight; + let context = canvas.getContext('2d'); + context.beginPath(); + originBounds.forEach((coord, index) => { + if (index === 0) { + context.moveTo(coord, originBounds[index + 1]); + } + if (index % 2 === 0) { + context.lineTo(coord, originBounds[index + 1]); + } + }); + context.closePath(); + context.fillStyle = '#ffffff'; + context.fill(); + let imgData = context.getImageData(0, 0, canvas.width, canvas.height); + let clipMat = that.cv.matFromImageData(imgData); + let dst1 = new that.cv.Mat(); + map.addSource(this.layerId, { + type: 'video', + urls: [url], + drawImageCallback(frame) { + let src = that.cv.matFromImageData(frame); + let dst = new that.cv.Mat(); + let M = that.cv.findHomography(srcTri, dstTri); + that.cv.warpPerspective(src, dst, M, that.dsize); + dst.copyTo(dst1, clipMat); + let newFrame = new ImageData(new Uint8ClampedArray(dst1.data), dst1.cols, dst1.rows); + src.delete(); + dst.delete(); + M.delete(); + return newFrame; + }, + coordinates: [ + [result[0], result[3]], + [result[2], result[3]], + [result[2], result[1]], + [result[0], result[1]] + ] + }); + + map.addLayer( + { + id: this.layerId, + type: 'raster', + source: this.layerId + } + ); + }); + } + + /** + * @function VideoLayer.prototype.moveLayer + * @description 移动图层。 + */ + moveLayer(beforeId) { + this.map.moveLayer(this.layerId, beforeId); + } + + /** + * @function VideoLayer.prototype.setVisibility + * @description 设置图层可见性。 + * @param {boolean} [visibility] - 是否显示图层。 + */ + setVisibility(visibility) { + const visible = visibility ? 'visible' : 'none'; + this.map.setLayoutProperty(this.layerId, 'visibility', visible); + } + } + \ No newline at end of file diff --git a/test/mapboxgl/overlay/VideoLayerSpec.js b/test/mapboxgl/overlay/VideoLayerSpec.js index 1b446f808..dcee7034e 100644 --- a/test/mapboxgl/overlay/VideoLayerSpec.js +++ b/test/mapboxgl/overlay/VideoLayerSpec.js @@ -9,11 +9,20 @@ describe('mapboxgl_VideoLayer', () => { var cv; beforeAll((done) => { cv = { + then(cb) { + setTimeout(function () { + cb(); + }, 200); + }, + CV_64FC1: 'CV_64FC1', CV_32FC2: 'CV_32FC2', matFromImageData: function () { return { delete: function () { + }, + copyTo: function () { + } } }, @@ -24,22 +33,44 @@ describe('mapboxgl_VideoLayer', () => { } }, matFromArray: function () { + return { + copyTo: function () { + }, + inv: function () { }, + delete: function () { }, + data64F: [200, 100], + cols: 2, + rows: 2, + data: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4] + } }, Mat: function () { return { + copyTo: function () { + + }, + inv: function () { }, delete: function () { }, + data64F: [200, 100], cols: 2, rows: 2, data: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4] } }, findHomography: function () { - + return { + delete: function () { } + } }, warpPerspective: function () { - } + }, + gemm: function () { }, + Rodrigues: function () { }, + projectPoints: function () { }, + multiply: function () { }, + subtract: function () { } }; testDiv = window.document.createElement('div'); testDiv.setAttribute('id', 'map'); @@ -95,11 +126,28 @@ describe('mapboxgl_VideoLayer', () => { it('init videoLayer', (done) => { var url = videoUrl; spyOn(cv, 'Size'); - spyOn(cv, 'findHomography'); spyOn(cv, 'warpPerspective'); var videoLayer = new VideoLayer({ url: url, opencv: cv, + clipRegion: [ + [0, 0], + [1920, 0], + [1920, 900], + [0, 900] + ], + videoParameters: { + fovX: 84, + fovY: 47, + centerX: 960, + centerY: 540, + pitch: -20, + roll: 0, + yaw: 2, + x: 11587478.810629973, + y: 3570800.195541344, + z: 154.50312 + }, extent: [ [116.14394400766855, 28.249134537249257], [116.143464581289, 28.252977295834056], @@ -110,7 +158,6 @@ describe('mapboxgl_VideoLayer', () => { videoLayer.onAdd(map); setTimeout(() => { expect(cv.Size).toHaveBeenCalled(); - expect(cv.findHomography).toHaveBeenCalled(); expect(cv.warpPerspective).toHaveBeenCalled(); expect(videoLayer.url).toBe(url); expect(videoLayer.videoDomId).not.toBeNull(); @@ -125,6 +172,24 @@ describe('mapboxgl_VideoLayer', () => { var videoLayer = new VideoLayer({ url: url, opencv: cv, + clipRegion: [ + [0, 0], + [1920, 0], + [1920, 900], + [0, 900] + ], + videoParameters: { + fovX: 84, + fovY: 47, + centerX: 960, + centerY: 540, + pitch: -20, + roll: 0, + yaw: 2, + x: 11587478.810629973, + y: 3570800.195541344, + z: 154.50312 + }, extent: [ [116.14394400766855, 28.249134537249257], [116.143464581289, 28.252977295834056], diff --git a/test/tool/mock_mapboxgl_map.js b/test/tool/mock_mapboxgl_map.js index ff9a05812..7df64ecdd 100644 --- a/test/tool/mock_mapboxgl_map.js +++ b/test/tool/mock_mapboxgl_map.js @@ -246,7 +246,13 @@ const Map = function (options) { enable: function () {} }; - this.project = function () { + this.project = function (latlng) { + if (latlng) { + return { + x: Math.floor(Math.random() * 800), + y: Math.floor(Math.random() * 600), + } + }; return { x: 500, y: 300