地图项目涉及知识点总结

news2024/9/21 22:16:01

序:最近做了一个在地图上标记点的项目,用户要求是在地图上显示百万量级的标记点,并且地图仍要可用(能拖拽,能缩放)。调研了不少方法和方案,最终实现了相对流畅的地图系统,加载耗时用户也可以接受,学到了不少知识,这里做一个总结

(PS:工作这几年,工程能力有没有提升不知道,数学能力绝对是飞速下降,简单的数据线性变换竟然要想半天,看来平时还是训练的太少了,不管是数据结构还是算法都完全没跟上)

技术栈

前端:vue + 高德地图js api 1.4
后端:springcloud
数据库:mongo
数据处理:pandas

一、前期调研,确定思路

用户提供的原始数据高达一亿,同屏出现的点的数量可能能到百万,就算把完全重合的点都去重也还是有近十万,且不说数据的时延,前端浏览器承载这么大的数据量本身就不太现实。经过各种调研,确认无论是百度地图、高德地图还是openlayers、leaflet这样的框架都很难支撑万级以上的数据量的绘制。就算绘制完成,地图也基本处于一个没法用的状态了。

和同事讨论后确定了整体方案:

1、数据处理

在不同的缩放等级下,对原始数据进行采样,以1x1或者2x2像素代表的经纬度范围为一组取其中一个随机数据(后来在用户的要求下改为求均值),在放到足够大(zoom16以上)后不再采样,展示原始数据

关于采样间隔的选择,在不同的缩放等级下进行计算。以zoom=11为例,一个像素表示的经纬度范围约为0.0006,那我们选择以0.001的间隔采样,绘图的时候以4个像素来表示一个点就比较合理。

如果想要更精细的绘图,那么就减小采样的间隔,与之相对的得到的数据规模就会更大,网页响应也就越慢。因此需要在性能和精准方面做一个取舍。

2、数据存储

对每个缩放等级下采样数据进行分表,随着缩放等级的增大,数据也越来越多,为了保证查询的效率采用四分法分表存储。
在zoom=11时,用一张表存储,zoom=12时用4张表,依此类推。到zoom=16时,分1024张表存储,后面我还建立了一张索引表,用来查询屏幕显示的经纬度范围所涉及的表有哪些。

数据库方面选择了mongodb,主要是看中了支持地理位置索引,可以快速查询地理位置上包围、相交的点的集合。

3、前端展示

经过测试,在高德地图和百度上绘制的海量点图层,当数据超过3w时地图的卡顿就会非常明显,想要容纳10w级的数据,不管是框架的性能和浏览器的内存都顶不住,因此想到在后端先把图片渲染好,然后传输到前端展示一个图层即可。

在缩放等级达到一定大小后,如zoom=16,屏幕内的数据规模去重后降低到1~2w这个数量级,再使用地图的标记物来绘制,用户可以和标记物进行交互。

基于以上思想开始了工作,中途学习很多新的东西,也踩了不少坑

二、数据处理

数据处理方面主要就使用pandas来读取用户提供的csv文件,然后经过采样后再保存到数据库里

pandas用了一个第三方库modin来加速,可以把电脑的cpu直接干满,大大节省了时间。1亿的数据两个小时不到就处理完成了

主要用的语法有

读取csv文件的指定列并指定分割符

df = pd.read_csv(path,sep='\t', usecols=[0, 1, 7, 9, 11, 13])

apply方法转换某两列的数据

df['grid_lon'], df['grid_lat'] = zip(*df[['Longitude', 'Latitude']].apply(lambda row: lat_lng_to_grid(row[0], row[1], grid_size), axis=1))

pandas分组求均值

concat_df.groupby(['grid_lon','grid_lat']).mean()

df舍弃列

df = df.drop(columns=['grid_lon', 'grid_lat','Longitude','Latitude'])

另外还涉及到python连接mongo数据库,使用pymongo

注意在插入数据前,需要先把df转换成字典列表

client = MongoClient("mongodb://admin@localhost:27017/?authSource=map", username="admin",
                         password="admin")
db = client["map"]
collection = db["map_index"]
collection.insert_one(map_data)

检查索引是否存在,如果不存在则建立2d索引

indexes = collection.list_indexes()
index_exists = any(index['name'] == index_name for index in indexes)
if not index_exists:
     collection.create_index([('lnglat', '2d')])

除此之外,如果数据使用的是国际坐标系wgs84,直接标记在国内的地图上是不准的,还需要转换成国内坐标系,即火星坐标系,转换方法在网上有比较详细的过程,这里就不赘述了

三、mongo数据库的使用

mongo数据库之前没有接触过,这是一个NoSQL的数据库,数据在库中被称为文档,每个文档的结构可以是不相同的,不需要有固定的结构,这也是NoSQL数据库的显著特征之一。

回到我们的项目,mongo数据库支持两种地理位置相关的索引,2d索引和2dSphere,二者的区别在于2d索引除了表示地理位置的经纬度之外,还用在平面地图相关的场景中,比如游戏的地图坐标等等。2dSphere则用于球形表面的位置存储。

在球形坐标上使用2d索引得到的结果不一定正确,在官方的文档中提到,如果要使用 $nearSphere(指定地理空间查询要按从最近到最远的顺序为其返回文档的点)这样和距离计算有关的查询,最好使用2dShere索引。在极点附近使用2d索引来判断位置也会出现错误。

两种索引的不同还体现在对查询语句的支持上。像$box这样的查询就只支持2d索引。具体可以阅读官方文档

https://www.mongodb.com/zh-cn/docs/manual/reference/operator/query-geospatial/

在这里插入图片描述

在本项目中不涉及到实际距离的计算,坐标位置也不在极点,因此使用2d索引也是可行的。在查询哪些区域和屏幕显示区域相交时,需要用到$geoIntersects,因此表的索引表建立的是2dSphere索引。

用到的查询语法

1、建立索引

db.sampled_11_0.createIndex({'lnglat':'2d'})

2、矩形区域内查询

db.sampled_11_0.find({
    "lnglat": {
    "$geoWithin": {
    "$box": [[113.728815,22.287244],[115.35306,23.015874]]
        }
    }
})

3、geoJson格式数据

注意 geoJson表示多边形的时候,起始点和结尾点必须相同

{"name":"mean_sampled_11_0",
 "zoom":zoom,
 "box":{
 	"type":"Polygon",
    "coordinates":[[[mg_min_lng,mg_min_lat],
    				[mg_min_lng,mg_max_lat],
    				[mg_max_lng,mg_max_lat],
    				[mg_max_lng,mg_min_lat],
    				[mg_min_lng,mg_min_lat]]]
                    }
                }

4、多边形相交

使用2dSphere索引时,查询语句中也是一个geoJson,下图中的示例代表一个多边形,查询与该多边形相交的数据

{
  <location field>: {
     $geoIntersects: {
        $geometry: {
           type: "Polygon" ,
           coordinates: [ <coordinates> ]
        }
     }
  }
}

四、数据查询&展示

数据完成处理并且入库后,我们搭好前后端的基本框架,就可以开始编写查询数据 -> 展示数据的代码了

前端

前端我们使用的是高德地图js api 1.4.5 不使用最新的2.0的原因是js api 2.0的缩放,zoom的步长不太好调整,而我们这次的项目只需要整数级别缩放即可。

地图api的使用,查看官方文档即可学会,并且官方还有在线调试的功能,非常的好用。如果遇到了问题,直接提工单,很快就会有工程师响应,不愧是大厂

初始化地图

initAMap() {
      window._AMapSecurityConfig = {
        securityJsCode: "*****************************",
      };
      AMapLoader.load({
        key: "******************************", // 申请好的Web端开发者Key,首次调用 load 时必填
        version: "1.4.15", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
        plugins: ["AMap.Scale"], //需要使用的的插件列表,如比例尺'AMap.Scale',支持添加多个如:['...','...']
      })
          .then((AMap) => {
            this.AMap = AMap
            this.map = new this.AMap.Map("container", {
              // 设置地图容器id
              resizeEnable: true,
              viewMode: "2D", // 是否为3D地图模式
              expandZoomRange:true,
              zoom: 10, // 初始化地图级别
              zooms: [3,20],
              center: [114.211168, 22.566057], // 初始化地图中心点位置
            });
            var scale = new AMap.Scale({visible: true,position: 'LT'})
            this.map.addControl(scale)
            this.map.on('movestart',this.clearMap)
            this.map.on('moveend',this.addPictureDebounce)
          })
    },

添加一个图片图层

var imageLayer = new this.AMap.ImageLayer({
                url: imageUrl, //图片 Url
                bounds: new this.AMap.Bounds([minLng,minLat], [maxLng,maxLat]), //图片范围大小的经纬度,传入西南和东北的经纬度坐标
                zIndex: zoom, //图层的层级
                zooms: [3, 20], //设置可见级别,[最小级别,最大级别]
              })
              this.map.add(imageLayer)

添加海量点图层

var massMarks = new this.AMap.MassMarks(this.pointData,
                {
                  zIndex: 100,
                  zooms: [3,20],
                  style: style,
                  opacity: 0.8
                }
            )
            massMarks.on('click',function (e){
              // 点击标记物的业务逻辑
            })
            massMarks.setMap(this.map)

后端

后端我们使用springcloud框架,需要实现的功能是,根据前端任意时刻的屏幕大小、经纬度范围等请求参数,找出范围内的数据,绘制一张png图片或者直接把数据发到前端。在这个项目中,我们在zoom小于16时进行后端绘图,在zoom大于等于16时将去重后的数据发送到前端。

java连接mongodb,需要引入第三方库

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

springCloud配置 如果只需要连接一个数据库,那么可以进行如下配置,mongoTemplate实例会自动加载

spring:
  application:
    name: map-service
  data:
    mongodb:
    	host:xxxxx
    	port:xxxxx
    	username:xxxxx
    	password:xxxxx
    	database:xxxxx

使用时直接注入mongoTemplate即可

@Autowired
private MongoTemplate mongoTemplate;

如果想要配置多个数据源,则需要自己写配置类来生成不同的mongoTemplate实例

spring:
  application:
    name: map-service
  data:
    mongodb:
      shenzhenTemplate:
        uri: mongodb://admin:admin@localhost:27017/map
      beijingTemplate:
        uri: mongodb://admin:admin@localhost:27017/map_beijing
public abstract class AbstractMongoClient {

    public MongoDatabaseFactory mongoDatabaseFactory(String uri) {
        return new SimpleMongoClientDatabaseFactory(uri);
    }
}

@Configuration
@Component
public class MongoTemplateConfigBJ extends AbstractMongoClient{
    @Value("${spring.data.mongodb.beijingTemplate.uri}")
    private String uri;

    @Bean(name = "beijingTemplate")
    public MongoTemplate mongoTemplate() {
        return new MongoTemplate(mongoDatabaseFactory(uri));
    }
}

@Configuration
@Component
public class MongoTemplateConfigSZ extends AbstractMongoClient{

    @Value("${spring.data.mongodb.shenzhenTemplate.uri}")
    private String uri;

    @Primary
    @Bean(name = "shenzhenTemplate")
    public MongoTemplate mongoTemplate() {
        return new MongoTemplate(mongoDatabaseFactory(uri));
    }
}

// 使用时
    @Autowired
    @Qualifier("shenzhenTemplate")
    private MongoTemplate mongoTemplate;

    @Autowired
    @Qualifier("beijingTemplate")
    private MongoTemplate beijingTemplate;

查询数据时,有多种方法可以使用,这里使用的是构造BasicDBObject的方法,这样写的好处是和mongo数据库的查询语句比较相似,容易理解

MongoCursor cursor是一个可迭代的对象,遍历它即可获取查询到的所有数据

BasicDBObject box = new BasicDBObject().append("$box",new double[][]{
                new double[]{minLng,minLat},
                new double[]{maxLng,maxLat}
        });
        BasicDBObject query = new BasicDBObject().append("lnglat", new BasicDBObject("$geoWithin",box));
        long start = System.currentTimeMillis();
        MongoCursor<Document> cursor =  mongoTemplate.getCollection("mean_sampled_11_0").find(query).iterator();
        Document document;
        while ( cursor.hasNext() ) {
            document = cursor.next();
        }

在前端请求发到后端时,由于在前面处理数据时我们做了分表处理,要先得到本次查询和哪些表相关。

public List<String> findCollectionNameList(Double minLng,Double maxLng,Double minLat,Double maxLat,Integer zoom,MongoTemplate mongoTemplate) {
		// 构建一个geoJson 查询哪些表的范围和这个矩形相交
        GeoJsonPolygon geoJsonPolygon = new GeoJsonPolygon(
                new Point(minLng,minLat),
                new Point(minLng,maxLat),
                new Point(maxLng,maxLat),
                new Point(maxLng,minLat),
                new Point(minLng,minLat)
        );
        if (zoom < 11) {
            zoom = 11;
        }
        if (zoom > 16) {
            zoom = 16;
        }
        // 这里用了另一种写法 直接查出来数据
        Criteria criteria = Criteria.where("box").intersects(geoJsonPolygon);
        List<LinkedHashMap> objects = mongoTemplate.find(new Query(criteria).addCriteria(new Criteria("zoom").is(zoom)),LinkedHashMap.class,"map_index");
        List<String> results = objects.stream().map(x ->(String) x.get("name")).collect(Collectors.toList());
        return results;
    }

得到了本次查询需要的表后,我们从这些表中查询数据。拿到数据后,我们再采取方法将数据转换为屏幕上的点,这里没有考虑地球投影导致的误差,直接使用线性变换找到每一个经纬度数据在屏幕上应该出现的位置

int screenX = (int) ((lng - minLng) * pixelsPerLngDegree);
int screenY = (int) (high / pixelPerGrid - (lat - minLat) * pixelsPerLatDegree);
if (screenX == width / pixelPerGrid) {
    screenX = width / pixelPerGrid -1;
}
if (screenY == high / pixelPerGrid) {
    screenY =  high / pixelPerGrid -1;
}
// 判断颜色
Integer color = judgeColor(value,pointValue);
// 计算像素点的位置
Integer location = screenY * width / pixelPerGrid + screenX;

随后,如果需要后端作图,我们还需要掌握java中生成png图片的方法。这里也有多种方法。

1、直接操作像素

生成一个width x height大小的数组,这个数组就代表了这幅图每个像素点的颜色,颜色可以用16进制数表示也可以用RGBA表示

我们根据之前换算出的数据,确定每一个像素点的颜色,然后给BufferedImage实例赋值,就完成了png图片的绘制

// 将所有数据转换到屏幕点上
        BufferedImage image = new BufferedImage(width, high, BufferedImage.TYPE_INT_ARGB);
        int[] pixels = new int[width * high];
        for (Integer location : pointMap.keySet()) {
            List<Integer> groupList = pointMap.get(location);
            int color = groupList.get(0);
            if (groupList.size() > 0) {
                color = groupList.get(ThreadLocalRandom.current().nextInt(0,groupList.size()));
            }
            // 由于4个像素代表一个格子 需要计算在第几列第几排
            int y = (int) location / (width / pixelPerGrid);
            int x = (int) location % (width / pixelPerGrid);
            // 得到第一个点的位置
            int firstPointLocation = width * pixelPerGrid * y + x * pixelPerGrid;
            // 绘制点上去
            for (int i = 0; i < pixelPerGrid;i ++) {
                for (int j =0;j < pixelPerGrid;j++) {
                    pixels[firstPointLocation + width * i + j] = COLOR_LIST[color];
                }
            }
        }
        image.setRGB(0, 0, width, high, pixels, 0, width);
        return image;

如果要导出图片,使用

ImageIO.write(image,"png",new File(filePath));

如果要发送回到前端,使用

OutputStream out = response.getOutputStream();
ImageIO.write(image,"png",out);

注:JDK8存在bug,ImageIO.write这个方法耗时很长,这个bug在JDK11完成了修复 如果想要使用需要升级JDK版本

2、使用Graphics2D

使用Graphics2D也可以完成画图,如

@Test
    public void drawCirclePng() throws IOException {
        Color circleColor = new Color(0xFFFF3300);
        BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = image.createGraphics();
        g2d.setColor(Color.white);
        g2d.fillRect(0,0,100,100);
        g2d.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setColor(circleColor);
        g2d.fillRect(5, 5, 1, 1);
        g2d.dispose();
        File outputFile = new File("rect_test.png");
        ImageIO.write(image, "png", outputFile);
    }

这里没有做细致研究,大致看了一下画图的方法,如果要用就得看官方文档了解下

五、优化体验

上面的步骤完成后,前端或将图片作为单独的图层显示出来,或直接进行海量点标记都已经完成了用户的需求,数据量一直控制在前端可以比较流畅运行的范围内。

另外我们还可以做一下优化,让用户体验更好

1、分块加载

虽然说我们已经做了采样 + 分表,但是由于原始数据规模巨大,数据库的单个集合数据量仍然是百万级,在这种情况下等待数据全部查询完,完成绘制恐怕还是要10s以上的时间,主要耗时在数据查询阶段。为此我们可以参考这些地图厂家的做法。

地图的每个瓦片都是分开查询加载的,我们在查询某个区域时,也可以把屏幕可见的经纬度范围分成多个区域,比如我这里分成了8块区域,同时发送请求查询数据,这样大大减少了用户等待的时间。

2、防抖 + 取消过期请求

我们在每次地图发生拖拽、缩放的时候,由于屏幕展示的经纬度范围发生了变化,因此需要重新获取数据。高德地图的api触发事件非常频繁,鼠标滚动几下能触发好几次事件。这时候如果每次都触发了请求,占用资源不说,得到的数据也已经过期了,如果还绘制在屏幕上,虽然是一闪而过,用户体验也是不好的。

接口防抖的技术已经非常成熟了,在各种下拉框筛选等可能会频繁触发接口的地方都有用到。这里我们使用一个第三方库来实现

import { debounce } from 'lodash'
addPictureDebounce: debounce(function () {
      this.addPicture()
    }, 500),
addPicture() {
	// 发起请求
}

如此一来 在事件触发后,如果500ms内没有再次触发该事件,才会调用发起请求的方法

500ms的时间很短,有的时候还是不可避免的触发了多次请求,或者上一次请求还没完成用户又拖动了地图该怎么办呢?我们理想的逻辑是,之前没完成的请求最好是丢弃掉。

这里使用axios的取消请求方法来实现

var controller = new AbortController()
this.abortControllerList.push(controller)
axios.post('map-service/map/getPointPng',{
 // 各种参数
},{responseType: 'blob',signal: controller.signal}).then(response => {
// 各种业务逻辑
})

这里我们将每个axios请求的congtroller都记录下,在下一次接口触发时,先把上次的请求全部丢弃

 this.abortControllerList.forEach(controller => {
        controller.abort()
      })

这样一来,过期的请求就不会在干扰我们的业务逻辑了

六、总结

这次项目,对我的各方面能力都是一次挑战,最难绷的还是各种数据的变换算不明白了,哎,真是菜的离谱

不管怎么样,还得勤学苦练,好好努力啊

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

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

相关文章

LLaMA 数据集

LLaMA的训练数据集来源多样&#xff0c;涵盖了多个不同的数据集和预处理步骤。以下是详细的描述&#xff1a; 公开数据来源和预处理 CommonCrawl [67%]&#xff1a; 使用CCNet管道&#xff08;Wenzek等人&#xff0c;2020年&#xff09;对2017年至2020年间的五个CommonCrawl转…

基于 Springboot + vue + java 美食分享平台(美食管理系统)

目录 &#x1f4da; 前言 &#x1f4d1;摘要 &#x1f4d1;操作流程 &#x1f4da; 系统架构设计 &#x1f4da; 数据库设计 &#x1f4ac; E-R表 &#x1f4ac; 用户表 &#x1f4ac; 美食分享文章表 &#x1f4ac;个人博客表 &#x1f4ac; 美食分类表 &#x1f4ac; …

如何根据项目需求选择采集卡及相关硬件

在选择适合的采集卡和硬件设备时&#xff0c;尤其是在要求高精度的应用场景中&#xff08;如压机测试中的1μm位移计&#xff09;&#xff0c;需要综合考虑多个因素。以下是选择硬件的几个关键原则&#xff1a; 1. 精度要求 对于需要高精度的应用&#xff0c;硬件的精度必须能…

Java代码批量处理sql语句

背景&#xff1a;数据源迁移&#xff0c;目标数据源和原始数据源的语法不同&#xff0c;要把建表语句全都改成新的语法。 一个个sql文件去替换实在是麻烦&#xff0c;可以把原始的sql文件放在一个文件夹&#xff0c;然后用程序一跑&#xff0c;改完语法的sql语句就放在新的文件…

口袋奇兵游戏攻略:云手机辅助战锤入侵策略指南!

在《口袋奇兵》中&#xff0c;战锤入侵是一个重要的游戏环节&#xff0c;了解如何有效地参与战锤入侵能够帮助玩家获取更多的资源和提升自己的战力。本文将详细介绍战锤入侵的策略和技巧&#xff0c;帮助玩家在战锤入侵活动中取得更好的成绩。除了找到强力的游戏辅助&#xff0…

miniconda+xinference的大模型推理部署指南

大模型相关目录 大模型&#xff0c;包括部署微调prompt/Agent应用开发、知识库增强、数据库增强、知识图谱增强、自然语言处理、多模态等大模型应用开发内容 从0起步&#xff0c;扬帆起航。 基于Dify的智能分类方案&#xff1a;大模型结合KNN算法&#xff08;附代码&#xff…

【视觉SLAM】 十四讲ch5习题

1.*寻找一个相机&#xff08;你手机或笔记本的摄像头即可&#xff09;&#xff0c;标定它的内参。你可能会用到标定板&#xff0c;或者自己打印一张标定用的棋盘格。 参考我之前写过的这篇博客&#xff1a;【OpenCV】 相机标定 calibrateCamera Code来源是《学习OpenCV3》18.…

喜报!CACTER实力入选《嘶吼2024网络安全产业图谱》多项领域

CACTER实力入选多项细分领域 7月16日&#xff0c;嘶吼安全产业研究院正式发布《嘶吼2024网络安全产业图谱》&#xff0c;旨在全面展示网络安全产业的构成及其重要组成部分&#xff0c;探索网络安全产业的竞争格局和发展前景。 CACTER凭借卓越的技术实力和可靠的产品服务&#…

[论文笔记] Pai-megatron-patch cpu-offload 改到 Qwen2

Add MPI Support for tp-comm-overlap and Cpu-Offload for Mcore Distrib… by jerryli1981 Pull Request #283 alibaba/Pai-Megatron-Patch GitHub 以上是在 llama-70B 上实现的 cpu-offload 方法。 下面是在主分支上&#xff0c;仿照 LLaMA-70B&#xff0c;在 Qwen2 上…

手把手教你搭建Docker私有仓库Harbor

1、什么是Docker私有仓库 Docker私有仓库是用于存储和管理Docker镜像的私有存储库。Docker默认会有一个公共的仓库Docker Hub&#xff0c;而与Docker Hub不同&#xff0c;私有仓库是受限访问的&#xff0c;只有授权用户才能够上传、下载和管理其中的镜像。这种私有仓库可以部署…

HarmonyOS工程目录结构

应用级配置文件app.json5 应用唯一标识、版本号、应用图标、应用名称等信息 模块级配置文件module.json5 oh-package.json5 三方库的管理 其他配置 用于编译构建&#xff0c;包括构建配置文件、编译构建任务脚本、混淆规则文件、依赖的共享包信息等。 build-profile.json…

Java学习Day9之数据库链接java

package aboutdb1; import java.sql.*; import java.util.Scanner; public class newDBsystem {private static Connection getConnection() throws Exception {Class.forName("com.mysql.cj.jdbc.Driver"); // 加载MySQL JDBC驱动Connection con DriverManager.get…

阿尔泰科技工业电脑IPC-8363工控机

概述&#xff1a; IPC-8363是一款支持 LGA 1200 Intel 10th/11th Generation Core™ i9/i7/i5/i3, Celeron and Pentium processor 的工业电脑。配置2组独立 SO-DIMM DDR4 2666/2933MHz内存&#xff0c;最大可扩展至128GB。 主要技术指标&#xff1a; 产品图示&#xff1a; 系…

php 小白新手从入门到精通教程(第3版)

前言 PHP&#xff08;PHP: Hypertext Preprocessor&#xff09;即“超文本预处理器”&#xff0c;是在服务器端执行的脚本语言&#xff0c;尤其适用于Web开发并可嵌入HTML中。PHP语法学习了C语言&#xff0c;吸纳Java和Perl多个语言的特色发展出自己的特色语法&#xff0c;并根…

qt初入门8:下拉框,输入框模糊查询,提示简单了解 (借助QCompleter)

实现一个简单的模糊查询的逻辑&#xff0c;输入框能提示相关项。 主要借助qt的QCompleter 类&#xff08; Qt 框架中提供的一个用于自动补全和模糊搜索的类&#xff09;&#xff0c;结合一些控件&#xff0c;比如QComboBox和QLineEdit&#xff0c;实现模糊查询的功能。 1&…

在线实习项目|泰迪智能科技企业级项目学习,暑期大数据人工智能学习

在线实习介绍 实习时间&#xff1a;每个项目周期七周左右 面向对象&#xff1a;大数据、计算机相关专业学生&#xff1b;大三、大四毕业年度学生 在线实习收获 1、获得项目实战技能&#xff0c;积累项目经验 2、获得在线实习证明 项目特点…

能源化工5G防爆终端能给行业带来什么重要作用?

在能源化工领域&#xff0c;5G防爆终端的引入无疑为行业带来了革命性的变革与重要作用。这些集成了先进5G通信技术和防爆设计的高端设备&#xff0c;不仅提升了生产作业的安全性&#xff0c;还极大地增强了运营效率与智能化水平。 高速、低延迟的5G网络为防爆终端提供了前所未有…

安全防御2

实验要求&#xff1a; 实验过程&#xff1a; 7&#xff0c;办公区设备可以通过电信链路和移动链路上网(多对多的NAT&#xff0c;并且需要保留一个公网IP不能用来转换)&#xff1a; 新建电信区&#xff1a; 新建移动区&#xff1a; 将对应接口划归到各自区域&#xff1a; 新建…

Java(二十二)---队列

文章目录 前言1.队列(Queue)的概念2.Queue的使用3.队列的模拟实现4.循环队列5.双端队列6.面试题[1. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/description/)[2. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/de…

VPN以及GRE和MGRE

VPN VPN — 是虚拟专用网络 通俗地说&#xff0c;就是通过虚拟的手段&#xff0c;将两个独立的网络&#xff0c;穿越一个公共网络进行连接&#xff0c;实现点到点专线的效果&#xff08;可以理解为&#xff1a;一个分公司通过公网和总公司建立点到点的专线连接&#xff09; 现…