创建JSON地图

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<style>
*{box-sizing: border-box;}
body{margin:0;padding:10px;font-size:14px;}
.top-bar{
  display:flex;
  gap:8px;
  margin-bottom:10px;
  align-items:center;
  flex-wrap:wrap;
}
.top-bar button,.top-bar input{
  padding:6px 10px;
  cursor:pointer;
}
.map-wrap{
  position:relative;
  width:1900px;
  height:900px;
  border:2px solid #666;
  overflow:hidden;
}
#bgImg{
  position:absolute;
  left:0;top:0;
  width:1900px;height:900px;
  object-fit:cover;
  display:block;
}
.heat-layer{
  position:absolute;left:0;top:0;width:100%;height:100%;
  z-index:2;pointer-events:none;display:none;
}
.heat-block{
  position:absolute;
  filter: blur(12px);
  border-radius:6px;
}
.zone-wrap{
  position:absolute;
  border:2px solid #999;
  background:rgba(150,150,150,0.05);
  z-index:3;
}
.zone-title{
  position:absolute;top:-22px;left:0;
  font-weight:bold;white-space:nowrap;
}
.factory-hot{
  position:absolute;
  border:2px dashed #0066ff;
  background:rgba(0,102,255,0.08);
  cursor:pointer;
  transition:all 0.2s;
}
.factory-hot:hover{background:rgba(0,102,255,0.2);}
.summary-tag{
  position:absolute;
  transform:translate(-50%,-50%);
  padding:4px 10px;border-radius:8px;
  color:#fff;font-weight:bold;white-space:nowrap;
  box-shadow:0 2px 8px rgba(0,0,0,0.15);
  cursor:pointer;
}
.sensor-hot{
  position:absolute;
  width:32px;height:32px;
  transform:translate(-50%,-50%);
  border-radius:50%;
  background:rgba(0,0,0,0.1);
  cursor:pointer;
  transition:all 0.2s;
}
.sensor-hot:hover{transform:translate(-50%,-50%) scale(1.2);}
.sensor-dot{
  width:12px;height:12px;border-radius:50%;
  margin:10px;
}
.drag-rect{
  position:absolute;
  border:1px dashed #f00;
  background:rgba(255,0,0,0.1);
  pointer-events:none;
  z-index:999;
}
.edit-modal{
  position:fixed;
  top:50%;left:50%;
  transform:translate(-50%,-50%);
  background:#fff;
  border:1px solid #ccc;
  padding:15px;
  box-shadow:0 0 20px #0003;
  z-index:9999;
  display:none;
}
.edit-modal input{
  width:280px;
  margin:6px 0;
  padding:5px;
  display:block;
}
.detail-popup{
  position:absolute;
  background:#fff;border:1px solid #ddd;
  padding:12px;box-shadow:0 3px 15px rgba(0,0,0,0.2);
  z-index:99;display:none;min-width:220px;
}
.del-btn{
  margin-top:8px;
  color:#fff;
  background:#f44336;
  border:none;
  padding:4px 8px;
  cursor:pointer;
}
</style>
</head>
<body>
<div class="top-bar">
  <input type="file" id="uploadImg" accept="image/*">
  <button id="drawRectFactory">矩形厂房(拖拽)</button>
  <button id="drawSensor">绘制传感器点位</button>
  <button id="stopDraw">停止绘制</button>
  <button id="toggleHeat">开关热力图层</button>
  <button id="toggleSensor">展开/收起点位</button>
  <button id="exportJson">导出配置JSON</button>
  <button id="importJson">导入配置JSON</button>
  <input type="file" id="jsonFile" accept=".json" style="display:none">
</div>

<div class="map-wrap" id="mapWrap">
  <img id="bgImg">
  <div class="heat-layer" id="heatLayer"></div>
  <div class="detail-popup" id="detailPopup"></div>
  <div class="drag-rect" id="dragRect"></div>
</div>

<div class="edit-modal" id="editModal">
  <h4>编辑配置信息</h4>
  <label>图层层级 z(数字越大越上层)</label>
  <input type="number" id="editZ" value="5">
  <label>名称</label>
  <input type="text" id="editName">
  <label>自定义参数doing</label>
  <input type="text" id="editDoing">
  <label>温度(仅传感器生效)</label>
  <input type="number" id="editTemp">
  <button id="saveEdit">保存</button>
  <button id="cancelEdit">取消</button>
</div>

<script>
$(function(){
  const $mapWrap = $("#mapWrap");
  const $bgImg = $("#bgImg");
  const $heatLayer = $("#heatLayer");
  const $popup = $("#detailPopup");
  const $dragRect = $("#dragRect");
  const $editModal = $("#editModal");

  // 全局配置:厂房、传感器完全分开存储
  let parkConfig = {
    zones:[]
  };

  // 绘制模式:rectFactory / sensor / null
  let drawMode = null;
  let isDrag = false;
  let dragStart = {x:0,y:0};
  let editTarget = null;
  // 删除标记:记录当前要删除的分区、下标、类型
  let delInfo = {zoneIdx: -1, type: "", idx: -1};

  let heatShow = false;
  let sensorShow = false;

  // 1.上传底图(仅前端预览,不存入JSON)
  $("#uploadImg").on("change",function(e){
    const file = e.target.files[0];
    if(!file)return;
    const reader = new FileReader();
    reader.onload = function(res){
      $bgImg.attr("src",res.target.result);
      renderAll();
    }
    reader.readAsDataURL(file);
  })

  // 切换绘制模式
  $("#drawRectFactory").click(()=>drawMode="rectFactory");
  $("#drawSensor").click(()=>drawMode="sensor");
  $("#stopDraw").click(()=>drawMode=null);

  // 矩形拖拽绘制厂房
  $mapWrap.mousedown(function(e){
    if(drawMode !== "rectFactory") return;
    const rect = $mapWrap[0].getBoundingClientRect();
    dragStart.x = e.clientX - rect.left;
    dragStart.y = e.clientY - rect.top;
    isDrag = true;
    $dragRect.css({left:dragStart.x,top:dragStart.y,width:0,height:0});
  })
  $mapWrap.mousemove(function(e){
    if(!isDrag || drawMode !== "rectFactory") return;
    const rect = $mapWrap[0].getBoundingClientRect();
    const curX = e.clientX - rect.left;
    const curY = e.clientY - rect.top;
    const w = Math.abs(curX - dragStart.x);
    const h = Math.abs(curY - dragStart.y);
    const l = Math.min(curX, dragStart.x);
    const t = Math.min(curY, dragStart.y);
    $dragRect.css({left:l,top:t,width:w,height:h});
  })
  $mapWrap.mouseup(function(e){
    if(drawMode !== "rectFactory" || !isDrag){
      $popup.hide();
      return;
    }
    isDrag = false;
    const x1 = parseInt($dragRect.css("left"));
    const y1 = parseInt($dragRect.css("top"));
    const x2 = x1 + parseInt($dragRect.css("width"));
    const y2 = y1 + parseInt($dragRect.css("height"));
    $dragRect.css({width:0,height:0});
    if(x2-x1 < 20 || y2-y1 <20)return;
    editTarget = {
      type:"rectFactory",
      data:{x1,y1,x2,y2,z:5,name:"矩形厂房",doing:""}
    };
    showEditModal();
  })

  // 点击画布绘制传感器点位
  $mapWrap.click(function(e){
    if(drawMode === "sensor"){
      const rect = $mapWrap[0].getBoundingClientRect();
      const x = parseInt(e.clientX - rect.left);
      const y = parseInt(e.clientY - rect.top);
      editTarget = {
        type:"sensor",
        data:{x,y,z:8,name:"传感器",doing:"",temp:25}
      };
      showEditModal();
      return;
    }
    if(!drawMode)$popup.hide();
  })

  // 弹出编辑框
  function showEditModal(){
    $("#editZ").val(editTarget.data.z||5);
    $("#editName").val(editTarget.data.name);
    $("#editDoing").val(editTarget.data.doing);
    $("#editTemp").val(editTarget.data.temp||25);
    $editModal.show();
  }
  $("#saveEdit").click(function(){
    const z = Number($("#editZ").val()) || 5;
    const name = $("#editName").val();
    const doing = $("#editDoing").val();
    const temp = parseFloat($("#editTemp").val())||25;
    editTarget.data.z = z;
    editTarget.data.name = name;
    editTarget.data.doing = doing;
    if(editTarget.type==="sensor") editTarget.data.temp = temp;

    // 初始化默认分区
    if(parkConfig.zones.length ===0){
      parkConfig.zones.push({
        zoneName:"默认园区分区",
        zoneX1:0,zoneY1:0,zoneX2:1920,zoneY2:1080,
        z:3,
        factories:[],
        sensors:[] // 传感器独立数组,与厂房完全分离
      })
    }
    const zone = parkConfig.zones[0];
    if(editTarget.type==="rectFactory"){
      zone.factories.push(editTarget.data);
    }else{
      zone.sensors.push(editTarget.data);
    }
    $editModal.hide();
    renderAll();
  })
  $("#cancelEdit").click(()=>$editModal.hide());

  // 删除元素
  $popup.on("click",".del-btn",function(){
    const zone = parkConfig.zones[delInfo.zoneIdx];
    if(delInfo.type === "factory"){
      zone.factories.splice(delInfo.idx,1);
    }else if(delInfo.type === "sensor"){
      zone.sensors.splice(delInfo.idx,1);
    }
    $popup.hide();
    renderAll();
  })

  // 温度配色工具
  function getTempColor(temp){
    if(temp<20)return "#2196F3";
    else if(temp>30)return "#f44336";
    return "#4CAF50";
  }

  function clearRender(){
    $(".zone-wrap,.factory-hot,.summary-tag,.sensor-hot,.heat-block").remove();
  }

  // 全局渲染
  function renderAll(){
    clearRender();
    parkConfig.zones.forEach((zone,zoneIdx)=>{
      const zw = zone.zoneX2 - zone.zoneX1;
      const zh = zone.zoneY2 - zone.zoneY1;
      // 分区层级z
      let $zone = $(`<div class="zone-wrap"><div class="zone-title">${zone.zoneName}</div></div>`)
        .css({
          left:zone.zoneX1,
          top:zone.zoneY1,
          width:zw,
          height:zh,
          zIndex: zone.z || 3
        });
      $mapWrap.append($zone);

      // ========== 渲染厂房区域 ==========
      zone.factories.forEach((fac,facIdx)=>{
        const tw = fac.x2 - fac.x1;
        const th = fac.y2 - fac.y1;
        const cx = fac.x1 + tw/2;
        const cy = fac.y1 + th/2;
        // 厂房层级z
        let $fac = $(`<div class="factory-hot"></div>`)
          .css({
            left:fac.x1,
            top:fac.y1,
            width:tw,
            height:th,
            zIndex: fac.z || 5
          });
        let $tag = $(`<div class="summary-tag">${fac.name} ${fac.doing}</div>`)
          .css({
            left:cx,
            top:cy,
            background:"#0066ff",
            zIndex: (fac.z || 5) + 1
          });
        // 点击弹窗带删除按钮
        $fac.add($tag).click(function(e){
          e.stopPropagation();
          delInfo = {zoneIdx, type:"factory", idx:facIdx};
          let html = `<h4>${fac.name} 备注:${fac.doing} | 层级z:${fac.z}</h4>`;
          html += `<button class="del-btn">删除此厂房</button>`;
          $popup.html(html).css({left:e.pageX,top:e.pageY}).show();
        })
        $mapWrap.append($fac).append($tag);

        // 热力色块
        let $heat = $(`<div class="heat-block"></div>`)
          .css({
            left:fac.x1,
            top:fac.y1,
            width:tw,
            height:th,
            background:"#0066ff",
            opacity:0.28
          })
        $heatLayer.append($heat);
      })

      // ========== 渲染独立传感器(单独数组,和厂房分离) ==========
      zone.sensors.forEach((sen,senIdx)=>{
        let c = getTempColor(sen.temp);
        let $sen = $(`<div class="sensor-hot"><div class="sensor-dot" style="background:${c}"></div></div>`)
          .css({
            left:sen.x,
            top:sen.y,
            display:sensorShow?"block":"none",
            zIndex: sen.z || 8
          })
          .click(function(e){
            e.stopPropagation();
            delInfo = {zoneIdx, type:"sensor", idx:senIdx};
            let html = `
              <h4>${sen.name}</h4>
              <p>层级z:${sen.z}</p>
              <p>doing:${sen.doing}</p>
              <p>温度:${sen.temp}℃</p>
              <button class="del-btn">删除此传感器</button>
            `;
            $popup.html(html).css({left:e.pageX,top:e.pageY}).show();
          })
        $mapWrap.append($sen);
      })
    })
  }

  // 热力图层开关
  $("#toggleHeat").click(function(){
    heatShow = !heatShow;
    $heatLayer.css("display",heatShow?"block":"none");
    $(this).text(heatShow?"关闭热力图层":"开启热力图层");
  })
  // 传感器显示开关
  $("#toggleSensor").click(function(){
    sensorShow = !sensorShow;
    $(".sensor-hot").css("display",sensorShow?"block":"none");
    $(this).text(sensorShow?"收起点位":"展开点位");
  })

  // 导出JSON:不含图片,厂房、传感器分开放
  $("#exportJson").click(function(){
    const output = JSON.parse(JSON.stringify(parkConfig));
    const jsonStr = JSON.stringify(output,null,2);
    const blob = new Blob([jsonStr],{type:"application/json"});
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "园区地图配置.json";
    a.click();
    URL.revokeObjectURL(url);
  })
  // 导入JSON
  $("#importJson").click(()=>$("#jsonFile").click());
  $("#jsonFile").on("change",function(e){
    const file = e.target.files[0];
    if(!file)return;
    const reader = new FileReader();
    reader.onload = function(res){
      try{
        parkConfig = JSON.parse(res.target.result);
        renderAll();
      }catch(err){
        alert("JSON格式错误");
      }
    }
    reader.readAsText(file);
  })

  renderAll();
})
</script>
</body>
</html>
发表在 None | 留下评论

简配GIS 网页显示

容器中使用图片地图全覆盖

JSON文件加载,各个点位相对图片容器位置创建可点击点位,可点击点位扩大10px创建点击事件(直接用css标签 然后来实现),调用模态框,可以添加AJAX事件实现交互。

循环上面步骤 大地图嵌套模态框显示小地图,或者显示点击交互事件,小地图也可以在上面创建交互实现地图的联合交互创建。

地图的相对点位如何创建?

下面列子非常好显示 了其中内容,创建一个图片容器div,1920×1080 大小,不可缩放。

图片100%铺满,通过each 以及定义好的css 创建点击位置,和方法

区域定位.css({left:item.x,top:item.y})

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<style>
.map-box{
  position:relative;
  width:1920px;
  height:1080px;
  border:1px solid #999;
}
.factory-map{
  width:1920px;
  height:1080px;
  display:block;
}
body{
  overflow:auto;
}
/* 热区:宽高32px,四周多出10px点击范围  用于创建点击事件 巧妙*/ 
.point-hot{
  position:absolute;
  width:32px;
  height:32px;
  transform:translate(-50%,-50%);
  cursor:pointer;
  background:transparent;
}
.point-dot{
  width:12px;
  height:12px;
  background:#f33;
  border-radius:50%;
  margin:10px;
}
.point-text{
  position:absolute;
  margin-top:-18px;
  font-size:12px;
  white-space:nowrap;
}
</style>
</head>
<body>
<div class="map-box" id="mapBox">
    <img class="factory-map" src="factory.jpg">
</div>
<script>
$(function(){
    let pointList = [
        {x:300,y:220,name:"一号车间"},
        {x:650,y:480,name:"水泵房"}
    ];
    const $mapBox = $("#mapBox");

    $.each(pointList,function(i,item){
        let $hot = $("<div class='point-hot'></div>")
            .css({left:item.x,top:item.y})
            .append("<div class='point-dot'></div><div class='point-text'>"+item.name+"</div>");
        
        // 点击热区任意位置,弹出点位名称
        $hot.click(function(e){
            e.stopPropagation();
            alert(item.name);
        });
        $mapBox.append($hot);
    })

    // 空白区域点击只打印坐标,不弹窗
    $mapBox.click(function(e){
        let r = $mapBox[0].getBoundingClientRect();
        let x = e.clientX - r.left,y = e.clientY - r.top;
        console.log("X:"+parseInt(x)+" Y:"+parseInt(y));
    })
})
</script>
</body>
</html>

创建区域

通过CSS来确定显示的样式

#CSS
/* 全局热力覆盖图层,底层、鼠标穿透不干扰点击 */
.heat-layer{
  position:absolute;
  left:0;
  top:0;
  width:100%;
  height:100%;
  z-index:2; /* 固定在厂房下方,底图上方 */
  pointer-events:none; /* 关键:色块不拦截鼠标点击厂房/传感器 */
  display:none;
}
/* 单块厂房热力色块 */
.heat-block{
  position:absolute;
  filter: blur(12px); /* 色块之间柔和渐变融合 */
  border-radius:6px;
}

创建区域 【显示区域使用添加DIV中加入上面的CSS样式,后用js读取json厂区的宽高后设置宽高 】区域定位参考上面,宽高定位width high

// 循环所有分区
parkConfig.zones.forEach((zone,zoneIdx)=>{
  // 循环当前分区全部厂房
  zone.factories.forEach((fac,facIdx)=>{
    // 厂房矩形宽高、坐标
    const tw = fac.x2 - fac.x1;
    const th = fac.y2 - fac.y1;

    // 根据业务温度获取厂房对应颜色
    function getTempColor(temp){
      if(temp<20)return "#2196F3";
      else if(temp>30)return "#f44336";
      return "#4CAF50";
    }
    // 这里简化统一厂房底色,你可替换为区域平均温度色
    const areaColor = "#0066ff";

    // 创建厂房热力覆盖色块,放入独立热力图层
    let $heat = $(`<div class="heat-block"></div>`)
      .css({
        left: fac.x1,
        top: fac.y1,
        width: tw,
        height: th,
        background: areaColor,
        opacity: 0.28 // 透明度,不遮挡底图
      })
    // 追加到热力覆盖图层
    $heatLayer.append($heat);
  })
})

点击事件及点击后触发

使用AJAX去后台读取内容,使用模态框来显示回传信息,可以在其内部添加一个请求的url【包含请求信息】通过GET请求回传JSON信息。

实时点位显示

同步动态点位信息,比如巡检ID显示,直接定期AJAX请求,然后显示点,可以在顶部显示z-index,数值高。依然适用ajax点击事件显示。(也可以直接展示信息)

跟正点击信息

A 直接URL 通过AJAX后请求后模态框展示信息

B 直接展示信息,直接模态框展示信息

doing中JSON显示其中 有url和 info两个参数分别去读取。

热力图

一、CSS 部分(分 3 块:画布基础、热力容器、圆形热力块)

1. 画布基础容器(固定 1900×900,承载底图 + 所有图层)

css

.map-wrap{
  position:relative;
  width:1900px;
  height:900px;
  border:2px solid #666;
  overflow:auto;
}
#bgImg{
  position:absolute;
  left:0;top:0;
  width:1900px;height:900px;
  object-fit:cover;
  display:block;
}

作用:

  • 固定画布尺寸 1900 宽 900 高,所有坐标基于这个画布;
  • position:relative 给内部热力、厂房、点位提供定位基准。

2. 热力总容器 .heat-layer(整层统一控制)

css

.heat-layer{
  position:absolute;
  left:0;
  top:0;
  width:100%;
  height:100%;
  z-index:2; /* 层级:底图之上、厂房之下 */
  pointer-events:none; /* 核心:鼠标穿透,不挡住点击厂房/传感器 */
  display:none; /* 默认隐藏,开关按钮控制显示 */
}

关键属性解释:

  1. position:absolute:铺满整个画布;
  2. z-index:2 图层顺序:底图 (0) < 热力层 (2) < 厂房 (4) < 传感器 (8);
  3. pointer-events:none:鼠标操作会直接穿过热力色块,底层交互完全不受影响;
  4. display:none:一键开关只改这个容器的 display,不用删所有色块。

3. 单个圆形热力块 .heat-block(核心实现圆形 + 渐变融合)

css

.heat-block{
  position:absolute;
  border-radius:50%; /* 强制正方形div变成正圆形 */
  filter: blur(20px); /* 高斯模糊,让圆形边缘向外虚化,多个圆融合渐变 */
}

两条核心:

  1. border-radius:50%:宽高相等的 div,圆角 50% = 标准圆形;
  2. filter: blur(20px):圆形边缘向外扩散虚化 20 像素,相邻圆形色块重叠后自动形成平滑过渡,这就是你看到整片渐变的来源;
  • 如果去掉 blur(20px),就能清晰看到一个个独立圆形热源。

二、JS 分步拆解(只看热力相关逻辑,分 5 步)

前置说明

数据结构:parkConfig.zones[0].sensors独立传感器数组,每个传感器包含 x,y,temp,热源只来源于这些点位。

步骤 1:工具函数 —— 根据温度返回热力颜色

js

运行

// 输入温度,返回半透明RGBA颜色
function tempToHeatColor(temp){
  if(temp <= 20) return "rgba(33,150,243,0.28)";  // 低温蓝色
  if(temp <= 30) return "rgba(76,175,80,0.28)";   // 常温绿色
  return "rgba(244,67,54,0.28)";                  // 高温红色
}

末尾 0.28 = 色块透明度,数值越低底色越淡,能看清底下厂区底图。

步骤 2:热力渲染主函数 renderHeatMap()(循环生成所有圆形色块)

js

运行

function renderHeatMap(){
  // 第一步:清空上一轮所有热力圆形,防止重复叠加
  $(".heat-block").remove();
  const $heatLayer = $("#heatLayer");

  // 遍历所有园区分区(当前只有1个默认分区)
  parkConfig.zones.forEach(zone=>{
    // 遍历【独立传感器数组】,每一个传感器生成1个圆形热力块
    zone.sensors.forEach(sen=>{
      // 1.获取当前传感器对应的温度颜色
      const fillColor = tempToHeatColor(sen.temp);

      // 2.根据温度计算圆形扩散半径:温度越高,圆越大
      const spreadRadius = Math.max(40, Math.min(130, sen.temp * 2.3));
      // Math.max(40,xxx):最小半径40,不会过小看不见
      // Math.min(130,xxx):最大半径130,不会铺满整张图

      // 3.创建圆形DOM热力块,定位到传感器坐标中心
      $("<div class='heat-block'></div>")
      .css({
        // 圆心在sen.x sen.y,向左/上偏移半径,保证圆形中心对准传感器点位
        left: sen.x - spreadRadius,
        top: sen.y - spreadRadius,
        // 宽高 = 两倍半径,形成完整圆形
        width: spreadRadius * 2,
        height: spreadRadius * 2,
        background: fillColor
      })
      // 把圆形色块追加到热力总容器内
      .appendTo($heatLayer);
    })
  })
}

坐标逻辑重点

  • 传感器坐标 sen.x sen.y 是圆形圆心
  • 圆形宽高 2*spreadRadius
  • left = 圆心x - 半径top = 圆心y - 半径,才能让圆形中心精准落在传感器绿点上。

步骤 3:全局渲染入口,每次重绘自动刷新热力

js

运行

function renderAll(){
  clearRender(); // 清空厂房、点位、旧热力
  // 渲染分区、厂房、传感器红点(原有逻辑省略)
  renderHeatMap(); // 最后执行热力圆形生成,刷新整张温度图层
}

触发时机:新建 / 删除传感器、导入 JSON、修改温度后,执行 renderAll(),热力自动重新绘制。

步骤 4:热力图层开关按钮逻辑(控制整层显示隐藏)

js

运行

let heatShow = false;
$("#toggleHeat").click(function(){
  heatShow = !heatShow;
  // 只修改热力总容器display,所有圆形色块一起显示/隐藏
  $heatLayer.css("display", heatShow ? "block" : "none");
  $(this).text(heatShow ? "关闭温度热力图层" : "开启温度热力图层");
})

不用逐个删除圆形 DOM,只控制外层容器,性能更高。

步骤 5:补充:关闭模糊,看清原生圆形(可选按钮逻辑)

新增变量控制模糊,切换后能清晰看到单个圆形:

js

运行

let heatBlurOn = true;
$("#toggleBlur").click(function(){
  heatBlurOn = !heatBlurOn;
  renderAll();
})
// 修改renderHeatMap内css赋值,动态控制blur
let cssStyle = {
  left: sen.x - spreadRadius,
  top: sen.y - spreadRadius,
  width: spreadRadius * 2,
  height: spreadRadius * 2,
  background: fillColor
};
if(heatBlurOn){
  cssStyle.filter = "blur(20px)";
}
$("<div class='heat-block'></div>").css(cssStyle).appendTo($heatLayer);
  • heatBlurOn=true:开启模糊,圆形边缘融合,看不出单独圆形;
  • heatBlurOn=false:无模糊,每个热源都是清晰独立圆形。

三、完整运行逻辑流程(从头到尾执行顺序)

  1. 上传厂区图片,填充 #bgImg 作为画布底图;
  2. 用户点击「绘制传感器点位」,在画布点击生成传感器,存入 zone.sensors
  3. 点击「开关热力图层」,heatShow=true.heat-layer 变为 display:block
  4. 页面执行 renderAll() → 调用 renderHeatMap()
  5. JS 循环读取所有独立传感器 x/y/temp
  6. 根据温度计算圆形半径、半透明颜色;
  7. 批量创建带 border-radius:50% 的圆形 div,放入热力容器;
  8. CSS blur(20px) 虚化所有圆形边缘,色块互相叠加融合,形成整张平滑温度图层;
  9. 热力层 pointer-events:none,底层厂房、传感器删除、弹窗功能完全不受遮挡。

四、你疑惑 “看不到圆形” 的根本原因

  1. 每个热源确实是标准圆形 div;
  2. 代码默认开启 filter: blur(20px),圆形边缘向外大面积虚化扩散;
  3. 多个圆形虚化区域重叠、颜色混合,肉眼看不到单独圆形轮廓,只看到整片渐变;
  4. 关闭 blur 模糊后,立刻能看到一个个独立圆形热源。
发表在 None | 留下评论

温湿度传感器 气压 气体

BME680

from bme680 import *
from machine import I2C, Pin
import time

bme = BME680_I2C(I2C(-1, scl=Pin(13), sda=Pin(14)))
print(bme.temperature, bme.humidity, bme.pressure, bme.gas)

关闭电加热丝 可以减少2摄氏度误差,修改源码bme680.py

        # gas measurements enabled
        #self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS])
        # gas measurements disabled
        self._write(_BME680_REG_CTRL_GAS, [0x00])

资料

https://gitee.com/mirrors_robert-hh/bme680-micropython

BME680 全包

后期

BMP280+AHT20

SGP-41 氮氧化合物 CO气体检测

发表在 None | 留下评论

蓝牙

广播 100ms 间隔高速率

import bluetooth, struct


#汉字一个3B,注意 最大8个汉字  100ms 广播高速率
def ble_name(dev_name):
    if len(dev_name)>26:
        return False
    ble = bluetooth.BLE()
    ble.active(False)
    ble.active(True)
    adv_data = struct.pack('BBB', 0x02, 0x01, 0x06)
    name_bytes = dev_name.encode()
    adv_data += struct.pack('B', 1 + len(name_bytes)) + b'\x09' + name_bytes
    ble.gap_advertise(None)
    ble.gap_advertise(100000, adv_data)
    print("广播启动,名称:", dev_name," 长度:",len(dev_name))
    return True

扫描 1s 定时版

from micropython import const
import bluetooth
import time

# 常量定义
_IRQ_SCAN_RESULT = const(5)
AD_TYPE_NAME_COMPLETE = const(0x09)
AD_TYPE_NAME_SHORT = const(0x08)

ble = bluetooth.BLE()
device_set = set()

def _parse_name(adv_data):
    i = 0
    while i < len(adv_data):
        ad_len = adv_data[i]
        if ad_len == 0:
            break
        ad_type = adv_data[i+1]
        payload = adv_data[i+2:i+1+ad_len]
        if ad_type in (AD_TYPE_NAME_COMPLETE, AD_TYPE_NAME_SHORT):
            try:
                return payload.decode("utf-8").strip()
            except:
                return "Unreadable"
        i += ad_len + 1
    return "NoName"

def _scan_callback(event, data):
    global device_set
    if event == _IRQ_SCAN_RESULT:
        _, _, _, _, adv_data = data
        name = _parse_name(bytes(adv_data))
        device_set.add(name)

def scan_ble_devices():
    """
    阻塞扫描1秒,返回周围蓝牙设备名称列表(去重)
    return: list[str]
    """
    global device_set
    device_set.clear()
    # 初始化蓝牙与回调
    ble.active(True)
    ble.irq(_scan_callback)
    # 扫描时长1000ms,高速抓包参数
    ble.gap_scan(1000, 30000, 28000, 1)
    # 阻塞等待最多1秒
    t_start = time.ticks_ms()
    while time.ticks_diff(time.ticks_ms(), t_start) < 1000:
        pass
    # 停止扫描、关闭蓝牙
    ble.gap_scan(None)
    ble.active(False)
    # 转列表返回
    return list(device_set)


# 调用示例
if __name__ == "__main__":
    names = scan_ble_devices()
    print("扫描到蓝牙设备:")
    for n in names:
        print("-", n)

#返回
#names = scan_ble_devices()

后台版本

开启服务 start_background_full_scan()然后返回扫描的设备列表 scan_ble_devices()

from micropython import const
import bluetooth
import time

# BLE常量
_IRQ_SCAN_RESULT = const(5)
AD_TYPE_NAME_COMPLETE = const(0x09)
AD_TYPE_NAME_SHORT = const(0x08)

ble = bluetooth.BLE()
# 存储结构 {设备名称: 最后收到广播时间戳}
dev_cache = dict()
SCAN_RUNNING = False
# 设备超时时间:1500ms未收到广播判定离开
DEV_TIMEOUT = 1500

def _parse_name(adv_data):
    """解析广播包中的蓝牙设备名称"""
    idx = 0
    data_len = len(adv_data)
    while idx < data_len:
        ad_len = adv_data[idx]
        if ad_len == 0:
            break
        ad_type = adv_data[idx + 1]
        payload = adv_data[idx + 2 : idx + 1 + ad_len]
        if ad_type in (AD_TYPE_NAME_COMPLETE, AD_TYPE_NAME_SHORT):
            try:
                return payload.decode("utf-8").strip()
            except Exception:
                return "Unreadable_Device"
        idx += ad_len + 1
    return "No_Name"

def ble_irq_callback(event, data):
    global dev_cache
    if event == _IRQ_SCAN_RESULT:
        _, _, _, _, adv_data = data
        dev_name = _parse_name(bytes(adv_data))
        now = time.ticks_ms()
        # 更新该设备最后活跃时间
        dev_cache[dev_name] = now

def start_background_full_scan():
    """启动后台永久高速扫描,匹配100ms间隔信标"""
    global SCAN_RUNNING
    if SCAN_RUNNING:
        return
    ble.active(True)
    ble.irq(ble_irq_callback)
    # gap_scan(永久扫描0, 扫描周期100000us=100ms, 监听窗口90000us=90ms, 主动扫描1)
    ble.gap_scan(0, 100000, 90000, 1)
    SCAN_RUNNING = True
    print("后台BLE扫描已启动 | 适配100ms间隔蓝牙信标")

def scan_ble_devices():
    """
    对外调用接口:瞬间返回当前在场蓝牙设备名称列表
    自动清理超时消失的设备,无阻塞
    return: list[str]
    """
    global dev_cache
    if not SCAN_RUNNING:
        return []
    now = time.ticks_ms()
    # 过滤超时设备
    valid_devs = []
    for name, last_tick in dev_cache.items():
        if time.ticks_diff(now, last_tick) < DEV_TIMEOUT:
            valid_devs.append(name)
    return valid_devs

def stop_ble_scan():
    """停止扫描、关闭蓝牙,程序结束时调用"""
    global SCAN_RUNNING
    if not SCAN_RUNNING:
        return
    ble.gap_scan(None)
    ble.active(False)
    SCAN_RUNNING = False
    dev_cache.clear()
    print("BLE扫描已关闭")

------------------- 测试示例 -------------------
if __name__ == "__main__":
    # 程序启动只执行一次,开启后台扫描
    start_background_full_scan()

    # 循环读取设备列表,随便高频调用无压力
    while True:
        dev_list = scan_ble_devices()
        print("==== 在场蓝牙设备 ====")
        if len(dev_list) == 0:
            print("无设备")
        else:
            for name in dev_list:
                print("-", name)
#         time.sleep_ms(500)

发表在 None | 留下评论

技术表

  • 蓝牙 广播(自定义31B)+扫描
  • lora 传输(大文件传输大于255B、中继)
  • E-INK显示
  • 按键IRQ
  • 温湿度传感器
  • 气体传感器
  • 24GHZ雷达检测人员
  • 录音模块
  • TTS模块(TTL)
发表在 None | 留下评论