Cesium气泡广告牌的实现方案

存稿:
又是被Leader折磨的一天,要我给无人机任务轨迹详情里加上飞行过程中拍摄的照片,嗯,具体效果嘛,就是下面的样子
在这里插入图片描述
好家伙,这是真能整活,这图片质感确定没自写材质?要我短时间搞定,那我肯定上阉割版啊,谁家好人陪你玩这么大。

Step 0:

思路:按照写广告牌div窗的方式,我写了一个div,专门用来放图片,然后将其计算屏幕坐标固定在坐标位置。

简直so easy,两个小时就搞定。
在这里插入图片描述
部分代码:

drawpictures(item){
      var viewer = this.viewer
      function convertToScreenCoordinates(lng, lat) {
          const position = Cesium.Cartesian3.fromDegrees(lng, lat);
          const screenPosition = Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene, position);

          return screenPosition;
      }

      const locations = [
          { lng: item.lng, lat: item.lat, imgUrl: item.fileUrl },
          { lng: item.lng + 0.001, lat: item.lat, imgUrl: item.fileUrl },
      ];

      locations.forEach(location => {
          const div = document.createElement('div');
          div.id = `popup-${location.lng}-${location.lat}`; 
          div.style.position = 'absolute';
          div.style.width = '100px';
          div.style.height = '100px';
          div.style.borderRadius = '50%'; // 圆形的边框
          div.style.border = '3px solid white'; // 边框颜色
          div.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
          div.style.backgroundImage = `url(${location.imgUrl})`;
          div.style.backgroundSize = 'cover';
          div.style.backgroundPosition = 'center';

          document.body.appendChild(div);

          const screenPos = convertToScreenCoordinates(location.lng, location.lat);

          div.style.left = `${screenPos.x - div.offsetWidth / 2}px`; // 居中定位
          div.style.top = `${screenPos.y - div.offsetHeight / 2}px`;

          const arrowDiv = document.createElement('div');
          arrowDiv.id = `arrow-${location.lng}-${location.lat}`; 
          arrowDiv.style.position = 'absolute';
          arrowDiv.style.width = '0';
          arrowDiv.style.height = '0';
          arrowDiv.style.borderLeft = '5px solid transparent';
          arrowDiv.style.borderRight = '5px solid transparent';
          arrowDiv.style.borderTop = '10px solid white'; // 箭头样式
          arrowDiv.style.left = `${screenPos.x - 5}px`;
          arrowDiv.style.top = `${screenPos.y + div.offsetHeight / 2}px`; // 箭头放在 div 的底部
          document.body.appendChild(arrowDiv);
      });


      viewer.scene.preRender.addEventListener(function () {
          locations.forEach(location => {
              const div = document.getElementById(`popup-${location.lng}-${location.lat}`);
              const arrowDiv = document.getElementById(`arrow-${location.lng}-${location.lat}`);

              // 检查 div 是否存在
              if (div && arrowDiv) {
                  // 计算屏幕坐标
                  const screenPos = convertToScreenCoordinates(location.lng, location.lat);
                  
                  // 更新 div 和箭头的位置
                  div.style.left = `${screenPos.x - div.offsetWidth / 2}px`;
                  div.style.top = `${screenPos.y - div.offsetHeight / 2}px`;
                  arrowDiv.style.left = `${screenPos.x - 5}px`;
                  arrowDiv.style.top = `${screenPos.y + div.offsetHeight / 2}px`;
              }
          });
      });
}

然而,突然发现有个无法跨越的bug,人家的图片广告牌随便挪动视角,位置都是在三维空间中相对不动的。
但我这个会根据经纬度计算屏幕坐标,一直存在于屏幕中,就很拉胯。

放弃,改用画布放置图片,然后一步步完善。

Step 1:

难点:广告牌不能同时加载两张图片让其重叠显示,也没有属性能直接加边框的,直接放入图片就只是一张图片。

思路:创建一张画布,将图片加入进去,加入白色边框,再生成一张新图片,这样一张带有白色边框图片就出来了。

在这里插入图片描述

部分代码:

drawpictures(item) {
        function createBillboardImageWithWhiteBorderAndArrow(imageUrl, borderSize) {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');

            const imageWidth = 45;  // 图片宽度
            const imageHeight = 45; // 图片高度
            const borderPadding = 5;  // 边框内边距,留出空间
            const arrowHeight = 10;   // 小三角形的高度

            // 设置 canvas 尺寸,确保边框和三角形有足够的空间
            const canvasSize = imageWidth + borderPadding * 2; // 总大小 = 图片大小 + 边框内边距
            canvas.width = canvasSize;
            canvas.height = canvasSize + arrowHeight; // 增加三角形的高度

            // 先绘制白色边框
            ctx.lineWidth = borderSize; // 设置边框宽度
            ctx.strokeStyle = 'white';  // 设置边框颜色
            ctx.strokeRect(borderSize / 2, borderSize / 2, canvasSize - borderSize, canvasSize - borderSize); // 绘制矩形边框

            // 创建 Image 对象并加载图片
            const img = new Image();
            img.crossOrigin = 'anonymous';  // 开启跨域

            // 设置图片链接
            img.src = imageUrl;

            return new Promise((resolve, reject) => {
                img.onerror = function () {
                    reject('图片加载失败');
                };

                img.onload = function () {
                    const offsetX = (canvasSize - imageWidth) / 2;
                    const offsetY = (canvasSize - imageHeight) / 2;

                    // 绘制图片到 canvas 中,确保图片在边框内居中
                    ctx.drawImage(img, offsetX, offsetY, imageWidth, imageHeight);

                    // 绘制小白色三角形指向底部
                    ctx.beginPath();
                    ctx.moveTo(canvasSize / 2, canvasSize + arrowHeight); // 三角形顶点位置 (底部中央)
                    ctx.lineTo(canvasSize / 2 - 10, canvasSize); // 左下角
                    ctx.lineTo(canvasSize / 2 + 10, canvasSize); // 右下角
                    ctx.closePath();
                    ctx.fillStyle = 'white';  // 设置三角形填充颜色
                    ctx.fill();

                    resolve(canvas); // 图片加载完成后,返回已绘制的 canvas
                };
            });
        }

        // 创建带白色边框和小三角形的 Billboard 图片并添加到视图
        createBillboardImageWithWhiteBorderAndArrow(item.fileUrl, 5)
            .then((canvas) => {
                // 图片加载并绘制完成后,创建 Billboard
                this.viewer.entities.add({
                    name: 'guiji-picture',
                    position: Cesium.Cartesian3.fromDegrees(item.lng, item.lat, item.alt),
                    billboard: {
                        image: canvas, // 使用绘制完成的 canvas 作为图像
                        color: Cesium.Color.WHITE.withAlpha(0.8),
                        height: 45,
                        width: 45,
                        sizeInMeters: false,
                        verticalOrigin: Cesium.VerticalOrigin.CENTER,
                        horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
                        pixelOffset: new Cesium.Cartesian2(-22.3, 1),
                        scale: 1.0,
                        show: true,
                    }
                });
            })
            .catch((error) => {
                console.error('图片加载失败:', error);
            });
    },

效果挺好,虽然美观上比不上人家,但我这么快就搞定了你还要什么飞机?

Step 2:

既然是预览图,直接用原图未免浪费性能,给它整上缩略图

// 创建缩略图
function createThumbnail(imageUrl, width, height) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.crossOrigin = 'anonymous';  // 开启跨域
        img.src = imageUrl;

        img.onload = () => {
            // 创建canvas元素
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            
            // 设置canvas的尺寸为目标缩略图的尺寸
            canvas.width = width;
            canvas.height = height;

            // 在canvas上绘制图像,自动调整为目标尺寸
            ctx.drawImage(img, 0, 0, width, height);
            
            // 获取缩略图
            canvas.toBlob((blob) => {
                resolve(URL.createObjectURL(blob)); // 返回缩略图的Blob URL
            }, 'image/jpeg', 0.6);  // 设置图片质量(0.6为60%质量)
        };

        img.onerror = () => reject('图片加载失败');
    });
}

Step 3:

一张图片搞定了,我想要更多图片怎么办?

思路:使用 Promise.all 批量处理,等待上一张图片变成带边框的缩略图然后通过广告牌绘制步骤全部完整,再开始处理下一张。

在这里插入图片描述

部分代码:

// 批量绘制广告牌
    drawMultiplePictures(items) {
        // 前置方法
        // ...
        
        // 使用 Promise.all 批量处理
        const promises = items.map(item => {
            // 创建缩略图并获取其 URL
            return createThumbnail(item.fileUrl, 45, 45) // 缩略图大小为 45x45
                .then((thumbnailUrl) => {
                    // 创建带白色边框和小三角形的 Billboard 图片并添加到视图
                    return createBillboardImageWithWhiteBorderAndArrow(thumbnailUrl, 5, 45, 45); // 使用缩略图 URL
                })
                .then((canvas) => {
                    // 图片加载并绘制完成后,创建 Billboard
                    this.viewer.entities.add({
                        name: 'guiji-picture',
                        position: Cesium.Cartesian3.fromDegrees(item.lng, item.lat, item.alt),
                        billboard: {
                            image: canvas, // 使用绘制完成的 canvas 作为图像
                            color: Cesium.Color.WHITE.withAlpha(0.8),
                            height: 45,
                            width: 45,
                            sizeInMeters: false,
                            verticalOrigin: Cesium.VerticalOrigin.CENTER,
                            horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
                            pixelOffset: new Cesium.Cartesian2(-22.3, 1),
                            scale: 1.0,
                            show: true,
                        }
                    });
                })
                .catch((error) => {
                    console.error('图片加载失败:', error);
                });
        });

        // 等待所有 Promise 完成
        Promise.all(promises).then(() => {
            console.log('所有广告牌已成功创建');
        }).catch((error) => {
            console.error('批量处理时出错:', error);
        });
    },

Step 4:

加入点击方法
在这里插入图片描述
部分代码:

// 批量绘制广告牌
    drawMultiplePictures(items) {
        // ...
        
        // 等待所有 Promise 完成
        Promise.all(promises).then(() => {
            console.log('所有广告牌已成功创建');
        }).catch((error) => {
            console.error('批量处理时出错:', error);
        });

        if (this.clickPicturesBillboard) {
            this.clickPicturesBillboard.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
        }

        // 创建新的事件监听器
        this.clickPicturesBillboard = new Cesium.ScreenSpaceEventHandler(this.viewer.canvas);
        this.clickPicturesBillboard.setInputAction((click) => {
            const pickedObject = this.viewer.scene.pick(click.position);
            if (Cesium.defined(pickedObject)) {
                const entity = pickedObject.id;
                if (entity && entity.properties) {
                    handleImageClick(entity.properties); 
                }
            }
        }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

        // 处理点击事件的回调方法
        function handleImageClick(properties) {
            console.log('点击广告牌的信息:', properties);
            alert(`点击了广告牌:\n图片URL: ${properties.fileUrl}\n经度: ${properties.lng}\n纬度: ${properties.lat}\n高度: ${properties.alt}`);
        }
    },

Step 5:

优化项:白色边框不参与缩略图处理;视角移动广告牌闪烁问题;鼠标悬浮在广告牌上缩略图变清晰;

完善后如下:
在这里插入图片描述
在这里插入图片描述

Step 5.1:

使用者习惯于同一个位置拍摄多张不同类型图片,需要我优化一下逻辑,不让广告牌重叠难以选中。

思路:设置同一位置广告牌加载时偏移高度;不同类型图片设置对应显隐控件;
在这里插入图片描述

解决完需求后,我又去看了一下人家的效果是否完美,发现同样存在一些疏漏。

  1. 高度线贴近模型(未穿模)就会连在模型上;
  2. 同一位置拍摄多张图片有微小偏移处理,但通过鼠标移动显然无法选中想要的那张。

由此,我这短期粗制滥造的功能也算够用了
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值