【数字孪生平台】使用 Three.js 以 3D 形式可视化日本新宿站地图

在本文中,我们将使用日本新宿站的室内地图数据,并使用 Three.js 将其进行 3D 可视化。更多精彩内容尽在数字孪生平台。

image.png

使用的数据

这次,我们将使用日本空间信息中心发布的“新宿站室内地图开放数据”的集成版本(ShapeFile)。

要素数据

在QGIS中显示

网络数据

在QGIS中显示

数据转换

所有这些数据都是 ShapeFile,我们可以使用 GDAL 命令将 ShapeFile 转换为 GeoJson。

mkdir geojsonfor f in *.shp; doogr2ogr -f GeoJSON -t_srs EPSG:6677 "geojson/${f%.*}.geojson" $f
done

使用 Three.js 绘制

创建场景

创建场景并添加相机和控制器。在本文中,我将把移动操作分配给 MapControls,将缩放操作分配给 TrackballControls。

import * as THREE from 'three';
import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js';const sizes = {width: window.innerWidth,height: window.innerHeight,
};// 创建画布
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);// 创建场景
const scene = new THREE.Scene();// 创建相机
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100000);
camera.position.set(-190, 280, -350);
scene.add(camera);// 创建控制器
const mapControls = new MapControls(camera, canvas);
mapControls.enableDamping = true;
mapControls.enableZoom = false;
mapControls.maxDistance = 1000;const zoomControls = new TrackballControls(camera, canvas);
zoomControls.noPan = true;
zoomControls.noRotate = true;
zoomControls.noZoom = false;
zoomControls.zoomSpeed = 0.5;// 渲染器
const renderer = new THREE.WebGLRenderer({canvas: canvas,alpha: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));// 当屏幕大小调整时,画布也会调整大小
const onResize = () => {// 获取尺寸大小const width = window.innerWidth;const height = window.innerHeight;// 调整渲染器大小renderer.setPixelRatio(window.devicePixelRatio);renderer.setSize(width, height);// 修正相机纵横比camera.aspect = width / height;camera.updateProjectionMatrix();
};
window.addEventListener('resize', onResize);const animate = () => {requestAnimationFrame(animate);const target = mapControls.target;mapControls.update();zoomControls.target.set(target.x, target.y, target.z);zoomControls.update();renderer.render(scene, camera);
};
animate();

另外,在 WebGLRenderer 选项中设置 alpha: true 以使背景透明并使用 CSS 将渐变应用于背景。

canvas {background-image: radial-gradient(#382c6e, #000000);
}

背景

创建 GUI 和组

在场景中绘制要素数据。这次我们按图层对要素进行分组,因此需要提前在场景中创建一个组,并将使用要素数据创建的对象添加到每个图层的组中。我们还将添加一个复选框 GUI,以便可以切换每个层次结构的显示。

import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';// 创建 dat.GUI 的实例
const gui = new GUI({ width: 150 });// 创建一个群组
const groupList = [4, 3, 2, 1, 0, -1, -2, -3];
const layers = ['4F', '3F', '2F', '1F', '0', 'B1', 'B2', 'B3'];groupList.forEach((num, i) => {const group = new THREE.Group();group.name = `group${num}`;scene.add(group);const key = `group${num}`;// 将复选框添加到 GUIgui.add({[key]: true,},key,).onChange((isVisible) => {scene.getObjectByName(key).visible = isVisible;}).name(layers[i]);
});

添加要素数据

往每个层组添加要素数据。由于要素数据是二维多边形数据,因此可以使用ExtrudeGeometry对其进行挤压,使其变为三维。虽然文件名中有层级结构信息,但没有高度信息,因此我们将根据特征数据的类型来划分高度。

另外,由于上述转换过程,特征数据已转换为 EPSG:6677,为了方便在threejs中查看,需要对要素进行偏移。首先,确定 EPSG:6677 上的哪个坐标点应作为场景的原点 0。这里我们将点 -12035.29、-34261.85(x,y) 与世界坐标原点 0 对齐,这样可以更好的查看。
image.png
当从要素数据的多边形坐标值创建ExtrudeGeometry时,通过从每个坐标点减去中心点([-12035.29,-34261.85])来应用偏移,这样要素就会被绘制到靠近场景原点的位置。

另外需要注意的是,Three.js(世界坐标)上的Y和Z向量方向与GIS(地理坐标)上的Y和Z向量方向相差90度,所以创建ExtrudeGeometry后,要从x轴开始。
地理坐标和threejs世界坐标
接下来我们从 GeoJson 创建 ExtrudeGeometry。将Space、Floor、Fixture数据写入每个数组并循环处理,使用函数getFloorNumber获取层级信息,使用函数loadAndAddToScene输入GeoJson信息、层级信息和高度值。

// Space数组
const SpaceLists = ['./ShinjukuTerminal/ShinjukuTerminal_B3_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_B2_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_B1_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_0_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_1_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_2_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_2out_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_3_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_3out_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_4_Space.geojson','./ShinjukuTerminal/ShinjukuTerminal_4out_Space.geojson',
];// Space加载
SpaceLists.forEach((geojson) => {const floorNumber = getFloorNumber(geojson, 'Space');if (floorNumber !== null) {loadAndAddToScene(geojson, floorNumber, 5);}
});// Floor数组
const FloorLists = ['./ShinjukuTerminal/ShinjukuTerminal_B3_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_B2_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_B1_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_0_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_1_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_2_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_2out_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_3_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_3out_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_4_Floor.geojson','./ShinjukuTerminal/ShinjukuTerminal_4out_Floor.geojson',
];// Floor加载
FloorLists.forEach((geojson) => {const floorNumber = getFloorNumber(geojson, 'Floor');if (floorNumber !== null) {loadAndAddToScene(geojson, floorNumber, 0.5);}
});// Fixture数组
const FixtureLists = ['./ShinjukuTerminal/ShinjukuTerminal_B3_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_B2_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_B1_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_0_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_2_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_2out_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_3_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_3out_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_4_Fixture.geojson','./ShinjukuTerminal/ShinjukuTerminal_4out_Fixture.geojson',
];// Fixture加载
FixtureLists.forEach((geojson) => {const floorNumber = getFloorNumber(geojson, 'Fixture');if (floorNumber !== null) {loadAndAddToScene(geojson, floorNumber, 5);}
});

getFloorNumber函数如下。我使用正则表达式提取文件名的数字部分,如果它包含“B”(位于地下),则返回负数。

// 使用正则表达式获取层数
const getFloorNumber = (geojson, type) => {const regex = new RegExp(`ShinjukuTerminal_([-B\\d]+)(out)?_${type}`);const match = geojson.match(regex);if (!match) return null;let floor = match[1].replace('B', '-');return parseInt(match[2] === 'out' ? floor.replace('out', '') : floor, 10);
};

loadAndAddToScene函数如下。使用 FileLoader 加载 GeoJson,使用名为 createExtrudedGeometry 的函数生成 ExtrudeGeometry,并通过从 ExtrudeGeometry 创建 EdgesGeometry 来创建轮廓立方多边形。此时,立方体多边形是垂直方向的,因此使用 applyMatrix4 将其旋转 90 度。
然后,我们决定在Y轴上放置每层信息的要素并将其添加到每层的Group中。


// 每层Y轴方向距离
const verticalOffset = 30;// 实例化文件加载器。获取JSON格式的数据
const loader = new THREE.FileLoader().setResponseType('json');// 加载文件并将其添加到场景中,排除那些没有几何信息的
const loadAndAddToScene = (geojson, floorNumber, depth) => {loader.load(geojson, (data) => {// Line材质const lineMaterial = new THREE.LineBasicMaterial({ color: 'rgb(255, 255, 255)' });// 排除那些没有几何信息的data.features.filter((feature) => feature.geometry).forEach((feature) => {// 生成ExtrudeGeometryconst geometry = createExtrudedGeometry(feature.geometry.coordinates, depth);// 旋转 90 度const matrix = new THREE.Matrix4().makeRotationX(Math.PI / -2);geometry.applyMatrix4(matrix);// 生成EdgesGeometryconst edges = new THREE.EdgesGeometry(geometry);const line = new THREE.LineSegments(edges, lineMaterial);line.position.y += floorNumber * verticalOffset - 1;// 添加到Groupconst group = scene.getObjectByName(`group${floorNumber}`);group.add(line);});});
};

createExtrudedGeometry 如下。如上所述,这里我们根据 GeoJson 的坐标创建一个 ShapeGeometry,它是 ExtrudedGeometry 的来源。每个坐标点都预先从中心减去其地理坐标。

此外,地理空间多边形数据需要顶点来闭合多边形,因此顶点数组的开头和结尾具有相同的顶点坐标(矩形多边形有 5 个顶点)。 ShapeGeometry 不需要最后一个顶点,因此我们跳过最后一个顶点的处理。ExtrudeGeometrydepth选项表示挤出高度。

// 场景中心的地理坐标 (EPSG:6677)
const center = [-12035.29, -34261.85];// 从多边形返回 ExtrudeGeometry 的函数
const createExtrudedGeometry = (coordinates, depth) => {const shape = new THREE.Shape();// 从多边形坐标创建形状coordinates[0].forEach((point, index) => {const [x, y] = point.map((coord, idx) => coord - center[idx]);if (index === 0) {// 移动到第一个点shape.moveTo(x, y);} else if (index + 1 === coordinates[0].length) {// 使用 closePath 关闭最后一个点shape.closePath();} else {// 其他 lineToshape.lineTo(x, y);}});return new THREE.ExtrudeGeometry(shape, {steps: 1,depth: depth,bevelEnabled: false,});
};

添加要素数据后的场景

添加网络数据

接下来,将网络数据(行人网络)添加到场景中。使用之前实例化的FileLoader加载上面转换的节点数据的GeoJson,获取nodeIdordinal(楼层信息),创建一个数组,并将其传递给名为createLink的函数。

// 从节点数据中获取node_id和层次结构(ordinal)
loader.load('./nw/Shinjuku_node.geojson', (data) => {const nodeIds = data.features.map((feature) => {return {node_id: feature.properties.node_id,ordinal: feature.properties.ordinal,};});// 创建行人网络creatingLink(nodeIds);
});

creatingLink函数如下所示。加载链接数据并从之前创建的数组中获取链接数据的起点和终点的层数结构,这是因为链接的数据不包含楼层信息(可以提前使用 QGIS 表连接添加楼层信息)。

虽然我们可以看到链接数据的起点和终点的层数结构,但是不知道之间的线的楼层落在哪里。因此,我们将准备一个条件分支,如果只找到起点的节点数据,则在起点的层高创建一条链接数据线,如果只找到终点的节点数据,则在终点的层高创建链接数据线。如果起点和终点的节点数据都找到了,如果起点和终点在同一层,就知道该链接数据线只存在于该层,但如果起点和终点在同一层的不同楼层,那么,我就暂时在中间层划一条线。

另外,链接数据线采用MeshLine,因为可以画宽线。在代码的后半部分,我特意将线分割成顶点位于两点之间的线,使用BufferGeometryUtils将它们合并,然后将它们添加到场景中,其原因将在后面解释。

import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import { MeshLine, MeshLineMaterial } from 'three.meshline';// 线材质
const linkMaterial = new MeshLineMaterial({transparent: true,lineWidth: 1,color: new THREE.Color('rgb(0, 255, 255)'),
});const meshLines = [];// 创建步行网络
const creatingLink = (nodeId) => {loader.load('./nw/Shinjuku_link.geojson', (data) => {data.features.forEach((feature) => {const coordinates = feature.geometry.coordinates;// 从节点数据中获取start_id和end_idconst start_id = nodeId.find((node) => node.node_id === feature.properties.start_id);const end_id = nodeId.find((node) => node.node_id === feature.properties.end_id);// 创建 3D 点数组const points = coordinates.map((point, index) => {let y;if (!start_id && !end_id) {// 如果没有start_id和end_id,则放在第0层y = 0;} else if (start_id && !end_id) {// 如果只有start_id,则将其放入start_id层次结构中y = start_id.ordinal;} else if (!start_id && end_id) {// 如果只有end_id,则将其放入end_id的层次结构中y = end_id.ordinal;} else {// 如果有 start_id 和 end_idif (index === 0) {// 对于第一点,将其放置在 start_id 层次结构中y = start_id.ordinal;} else if (index === coordinates.length - 1) {// 如果是最后一个点,则将其放入end_id的层次结构中y = end_id.ordinal;} else if (start_id.ordinal === end_id.ordinal) {// 如果 start_id 和 end_id 位于同一层次结构中,则将它们放入该层次结构中y = end_id.ordinal;} else {// 如果start_id和end_id位于不同的层次结构中,则将它们放在中间层次结构中y = Math.round((start_id.ordinal + end_id.ordinal) / 2);}}return new THREE.Vector3(point[0] - center[0], y * verticalOffset + 1, -(point[1] - center[1]));});// 从point数组创建MeshLinepoints.forEach((point, index) => {// 如果是最后一点,则结束流程if (index + 1 === points.length) return;// 创建MeshLine。在两点之间创建单独的网格线const geometry = new THREE.BufferGeometry().setFromPoints([point, points[index + 1]]);const line = new MeshLine();line.setGeometry(geometry);// 添加到 MeshLine 数组const mesh = new THREE.Mesh(line, linkMaterial);meshLines.push(mesh.geometry);});});// 合并MeshLineconst linkGeometry = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(meshLines), linkMaterial);linkGeometry.name = 'link';// 添加到场景scene.add(linkGeometry);});
};

行人网络添加后

编写着色器

接下来我们使行人网络连接线流动起来。这次我们使用的数据有两种模式:(1)双向和(2)从起点到终点的方向,所以我们准备了两个简单的着色器应用于网格线。

首先,从之前定义的 MeshLineMaterial 中删除颜色信息。然后,使用onBeforeCompile添加着色器以覆盖MeshLine中现有的着色器。


// 添加着色器
linkMaterial.onBeforeCompile = (shader) => {// 将 uniforms 添加到 userDataObject.assign(shader.uniforms, linkMaterial.userData.uniforms);const keyword1 = 'void main() {';shader.vertexShader = shader.vertexShader.replace(keyword1,/* GLSL */ `varying vec2 vUv;attribute float uDistance;attribute float uDirection;varying float vDistance;varying float vDirection;${keyword1}`,);// 替换并添加到着色器const keyword2 = 'vUV = uv;';shader.vertexShader = shader.vertexShader.replace(keyword2,/* GLSL */ `${keyword2}vUv = uv;vDistance = uDistance;vDirection = uDirection;`,);const keyword3 = 'void main() {';shader.fragmentShader = shader.fragmentShader.replace(keyword3,/* GLSL */ `uniform float uTime;varying float vDirection;varying float vDistance;varying vec2 vUv;${keyword3}`,);// 替换并添加到着色器const keyword4 = 'gl_FragColor.a *= step(vCounters, visibility);';shader.fragmentShader = shader.fragmentShader.replace(keyword4,/* GLSL */ `${keyword4}vec2 p;p.x = vUv.x * vDistance;p.y = vUv.y * 1.0 - 0.5;float centerDistY = p.y; // 距中心 Y 距离float offset = abs(centerDistY) * 0.5; // 控制对角线角度float time = uTime;// 更改中心上方和下方的对角线方向if(centerDistY < 0.0) {if(vDirection == 1.0){time = -uTime;offset = -offset;}else if(vDirection == 2.0) {offset = offset;}}// 使用 mod 函数和基于距中心 y 距离的偏移生成线float line = mod(p.x - time + offset, 1.9) < 0.9 ? 1.0 : 0.0;vec3 mainColor;// 根据方向改变颜色if(vDirection == 1.0) {mainColor = vec3(0.0, 1.0, 1.0);} else if(vDirection == 2.0) {mainColor = vec3(1.0, 1.0, 0.0);}vec3 color = mix(mainColor, mainColor, line);gl_FragColor = vec4(color, line * 0.7);`,);
};

然后将其添加到createLink函数中。为了匹配每条 MeshLine 的 UV 坐标的长宽比,获取该线的两点之间的距离,并将名为 uDirection 的属性变量传递给着色器。此外,有关连接线数据方向的信息也会使用名为 uDirection 的属性变量传递到着色器。我添加了一个名为 uTime 的uniform变量来制作动画。

               // 计算两点之间的距离const distance = point.distanceTo(points[index + 1]);// 获取MeshLine的顶点数const numVerticesAfter = line.geometry.getAttribute('position').count;// 根据顶点数量生成距离数组,并使用 setAttribute 添加顶点属性,用于计算UV坐标的纵横比const distances = new Float32Array(numVerticesAfter).fill(distance);line.setAttribute('uDistance', new THREE.BufferAttribute(distances, 1));// 根据顶点数量生成方向数组,并使用 setAttribute 添加顶点属性,代表连接线数据的方向const directions = new Float32Array(numVerticesAfter).fill(feature.properties.direction);line.setAttribute('uDirection', new THREE.BufferAttribute(directions, 1));// 将uTime(时间)添加到uniforms 变量中,用于动画Object.assign(linkMaterial.userData, {uniforms: {uTime: { value: 0 },},});

接下来,在animate函数中编写添加到uTime的过程。

   // 行人网络动画if (linkMaterial.uniforms.uTime) {linkMaterial.uniforms.uTime.value += 0.1;}

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_1575765_75d57b39-c3a5-0e31-4045-3ab1904d3baa.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/779001.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络安全新视角:数据可视化的力量

在当今数字化时代&#xff0c;网络安全已成为各大企业乃至国家安全的重要组成部分。随着网络攻击的日益复杂和隐蔽&#xff0c;传统的网络安全防护措施已难以满足需求&#xff0c;急需新型的解决方案以增强网络防护能力。数据可视化技术&#xff0c;作为一种将复杂数据转换为图…

Weblogic10.3.6补丁升级

由oracle官方发布的漏洞公告&#xff0c;对weblogic进行补丁升级。本文介绍的是weblogic安装的版本为10.3.6.0&#xff0c;如果你使用的是其他版本的请根据实际版本来选择下载 对于weblogic补丁升级&#xff0c;具体步骤如下&#xff1a; 1、首先需要下载weblogic补丁程序 2…

简单了解原型模式

什么是原型模式 区别于单例模式&#xff0c;原型模式的一个类可以有多个实例化的对象。 原型模式通过拷贝来产生新的对象&#xff0c;而不是new&#xff0c;并且可以根据自己的需求修改对象的属性。 实现Cloneable接口实现拷贝 而拷贝又分为浅拷贝和深拷贝&#xff0c;两者在…

AI赋能微服务:Spring Boot与机器学习驱动的未来应用开发

&#x1f9d1; 作者简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟。提供嵌入式方向的学习指导、简历面…

HarmonyOS入门--页面和自定义组件生命周期

文章目录 页面和自定义组件生命周期页面生命周期组件生命周期生命周期的调用时机 页面和自定义组件生命周期 生命周期流程如下图所示&#xff0c;下图展示的是被Entry装饰的组件&#xff08;首页&#xff09;生命周期。 自定义组件和页面的关系&#xff1a; 自定义组件&…

[flask]http请求//获取请求体数据

import jsonfrom flask import Flask, requestapp Flask(__name__)app.route("/form1", methods["post"]) def form1():"""获取客户端请求的请求体[表单]:return:""""""获取表单数据请求url&#xff1a;&qu…

网络安全卷么?

官方宣传的是对的网络安全现在是朝阳行业&#xff0c;缺口是很大 不过网络安全行业就是需要技术很多的人达不到企业要求才导致人才缺口大 初级的现在有很多的运维人员转网络安全&#xff0c;初级也会慢慢的卷起来&#xff0c;但是岗位多不用怕&#xff0c;以后各大去也都会要网…

Git Fork后的仓库内容和原仓库保持一致

Git Fork后的仓库内容和原仓库保持一致 ①Fork原仓库内容到自己仓库 ②将项目内容下载到本地 ③使用git命令获取原仓库内容&#xff0c;将原仓库的最新内容合并到自己的分支上并推送 下面从第三步开始演示~ 这里以码云上的若依项目为演示项目 ③使用git命令获取原仓库内容 …

基于龙芯2k1000 mips架构ddr调试心得(二)

1、内存控制器概述 龙芯处理器内部集成的内存控制器的设计遵守 DDR2/3 SDRAM 的行业标准&#xff08;JESD79-2 和 JESD79-3&#xff09;。在龙芯处理器中&#xff0c;所实现的所有内存读/写操作都遵守 JESD79-2B 及 JESD79-3 的规定。龙芯处理器支持最大 4 个 CS&#xff08;由…

Spring Transaction 指定事务管理器问题

一&#xff0c;单个数据源&#xff0c;单个事务管理器与Transactional默认事务管理器名称不一致问题 在平时代码中使用声明性事务时&#xff0c;直接在方法上面加注解即可&#xff0c;如下 Transactional(rollbackFor Exception.class) 并没有指定事务管理器&#xff0c;为…

探索直播美颜技术:计算机视觉在美颜SDK开发中的应用

下文&#xff0c;小编将深入探讨美颜技术在计算机视觉领域中的应用&#xff0c;特别是美颜SDK的开发过程&#xff0c;并剖析其技术原理和实现方法。 一、美颜技术的发展 这些算法往往难以满足用户对高质量美颜效果的需求&#xff0c;因此需要更加先进的技术手段来实现。 二、…

map与set容器常见操作详解(含示例代码及注意事项)

&#x1f389;个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名乐于分享在学习道路上收获的大二在校生 &#x1f648;个人主页&#x1f389;&#xff1a;GOTXX &#x1f43c;个人WeChat&#xff1a;ILXOXVJE &#x1f43c;本文由GOTXX原创&#xff0c;首发CSDN&…

LeetCode-热题100:73. 矩阵置零

题目描述 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例 1&#xff1a; 输入&#xff1a; matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&#xff1a; [[1,0,1],[0,0,0],[1,0,1]] 示例 2&…

企微侧边栏开发(内部应用内嵌H5)

一、背景 公司的业务需要用企业微信和客户进行沟通&#xff0c;而客户的个人信息基本都存储在内部CRM系统中&#xff0c;对于销售来说需要一边看企微&#xff0c;一边去内部CRM系统查询&#xff0c;比较麻烦&#xff0c;希望能在企微增加一个侧边栏展示客户的详细信息&#xf…

常见手撕项目C++

常见手撕项目C 设计模式单例模式饿汉模式懒汉模式 设计模式 单例模式 单例模式是一种常用的软件设计模式&#xff0c;其目的是确保一个类只有一个实例&#xff0c;并提供一个全局访问点来获取该实例。 优点&#xff1a; 资源控制&#xff1a;单例模式能够确保一个类只有一个实…

淘宝订单中的涉及红包检测、优惠券检测方案|工具|API

首先&#xff0c;检测订单红包的核心价值是什么&#xff1f; “红包的本质就是薅平台羊毛&#xff1a;不用怀疑&#xff0c;平台对于这种损害平台利益的行为肯定是最高等级的稽查”。那么&#xff0c;在日常运营中&#xff0c;需要尽可能过滤这类订单。 其次&#xff0c;如何使…

【数据结构 | 图论】如何用链式前向星存图(保姆级教程,详细图解+完整代码)

一、概述 链式前向星是一种用于存储图的数据结构&#xff0c;特别适合于存储稀疏图&#xff0c;它可以有效地存储图的边和节点信息&#xff0c;以及边的权重。 它的主要思想是将每个节点的所有出边存储在一起&#xff0c;通过数组的方式连接&#xff08;类似静态数组实现链表…

慧天【HTWATER】:水文水动力模型的革命性工具,城市内涝的精准解决方案

城市内涝水文水动力模型介绍 在城市排水防涝规划过程中&#xff0c;水文水动力耦合模型已经成为一种不可或缺的分析工具。在模型建立、城市内涝风险评估、排水系统性能诊断以及海绵城市规划等方面&#xff0c;内涝耦合模型提供了相应的模拟及分析工具&#xff1a; 1.1丰富的数…

前端学习<二>CSS基础——09-CSS案例讲解:博雅互动

前言 CSS已经学了一些基础内容了&#xff0c;我们来讲解一个小案例吧。以博雅互动的官网首页举例。 版心 首页的版心如下&#xff1a; 这里我们要普及一个概念&#xff0c;叫“版心”。版心是页面中主要内容所在的区域。 比如说&#xff0c;网站左上角的logo&#xff0c;设计…

Flutter 开发学习笔记(0):环境配置

文章目录 前言开发需求环境配置运行出现问题我运行也是解决了很久的问题镜像源设置为清华的镜像源&#xff08;不知道有没有影响&#xff09;使用JDK21&#xff0c;JDK版本不能低于JDK11手动下载flutter 对应的gradle 运行成功&#xff01; 前言 我最近一直在用Uniapp写代码&a…