From dab56435418814c2531bc2c603a0d2ea8b3ed1b7 Mon Sep 17 00:00:00 2001 From: lin <648540858@qq.com> Date: Thu, 30 Oct 2025 23:52:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=A0=87=E5=87=86=E7=9F=A2?= =?UTF-8?q?=E9=87=8F=E5=9B=BE=E5=B1=82=E7=9A=84=E5=88=9B=E5=BB=BA=E5=92=8C?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 18 ++++ .../iot/vmp/conf/GlobalResponseAdvice.java | 4 + .../gb28181/controller/ChannelController.java | 20 +++- .../gb28181/dao/CommonGBChannelMapper.java | 3 + .../gb28181/dao/provider/ChannelProvider.java | 14 ++- .../gb28181/service/IGbChannelService.java | 2 + .../service/impl/GbChannelServiceImpl.java | 100 +++++++++++++++++- web/src/views/common/MapComponent.vue | 67 ++++++++++-- web/src/views/map/index.vue | 16 ++- 9 files changed, 223 insertions(+), 21 deletions(-) diff --git a/pom.xml b/pom.xml index 41538cab8..e6405c858 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,10 @@ true + + ECC + https://maven.ecc.no/releases + @@ -392,6 +396,20 @@ 1.78.1 + + + no.ecc.vectortile + java-vector-tile + 1.4.1 + + + + + org.locationtech.jts + jts-core + 1.18.2 + + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java b/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java index af12ecabd..2db3bb528 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/GlobalResponseAdvice.java @@ -38,6 +38,10 @@ public class GlobalResponseAdvice implements ResponseBodyAdvice { } } + if (selectedContentType.equals(MediaType.parseMediaType("application/x-protobuf"))) { + return body; + } + if (body instanceof WVPResult) { return body; } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java index 83c63953d..d7d3bd5a6 100755 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/ChannelController.java @@ -23,6 +23,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.*; @@ -498,6 +502,20 @@ public class ChannelController { channelService.resetLevel(); } + @Operation(summary = "为地图提供标准的mvt图层", security = @SecurityRequirement(name = JwtUtils.HEADER)) + @GetMapping(value = "/map/tile/{z}/{x}/{y}", produces = "application/x-protobuf") + @Parameter(name = "geoCoordSys", description = "地理坐标系, WGS84/GCJ02") + public ResponseEntity getTile(@PathVariable int z, @PathVariable int x, @PathVariable int y, String geoCoordSys){ - + try { + byte[] mvt = channelService.getTile(z, x, y, geoCoordSys); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/x-protobuf")); + headers.setContentLength(mvt.length); + return new ResponseEntity<>(mvt, headers, HttpStatus.OK); + } catch (Exception e) { + log.error("构建矢量瓦片失败: z: {}, x: {}, y:{}", z, x, y, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java index fd7a73900..5cfb1e6c9 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java @@ -673,4 +673,7 @@ public interface CommonGBChannelMapper { @Update("UPDATE wvp_device_channel SET map_level=null") int resetLevel(); + @SelectProvider(type = ChannelProvider.class, method = "queryCameraChannelInBox") + List queryCameraChannelInBox(@Param("minLon") double minLon, @Param("maxLon") double maxLon, + @Param("minLat") double minLat, @Param("maxLat") double maxLat); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java index 679c89cae..12b4a97c7 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java @@ -527,10 +527,13 @@ public class ChannelProvider { " where data_type=#{dataType} and data_device_id=#{dataDeviceId} AND device_id=#{deviceId}"; } - public String queryOnlineListsByGbDeviceId(Map params ){ + public String queryCameraChannelInBox(Map params ){ StringBuilder sqlBuild = new StringBuilder(); sqlBuild.append(BASE_SQL_TABLE_NAME); - sqlBuild.append(" where wdc.channel_type = 0 AND coalesce(wdc.gb_status, wdc.status) = 'ON' AND wdc.data_type = 1 AND data_device_id = #{deviceId}"); + sqlBuild.append(" where coalesce(wdc.gb_longitude, wdc.longitude) > #{minLon} " + + "AND coalesce(wdc.gb_longitude, wdc.longitude) <= #{maxLon} " + + "AND coalesce(wdc.gb_latitude, wdc.latitude) > #{minLat} " + + "AND coalesce(wdc.gb_latitude, wdc.latitude) <= #{maxLat}"); return sqlBuild.toString(); } @@ -570,6 +573,13 @@ public class ChannelProvider { return sqlBuild.toString(); } + public String queryOnlineListsByGbDeviceId(Map params ){ + StringBuilder sqlBuild = new StringBuilder(); + sqlBuild.append(BASE_SQL_TABLE_NAME); + sqlBuild.append(" where wdc.channel_type = 0 AND coalesce(wdc.gb_status, wdc.status) = 'ON' AND wdc.data_type = 1 AND data_device_id = #{deviceId}"); + return sqlBuild.toString(); + } + public String queryListForSy(Map params ){ StringBuilder sqlBuild = new StringBuilder(); sqlBuild.append(BASE_SQL_FOR_CAMERA_DEVICE); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java index 1ef60cf9a..2e9fd1538 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java @@ -107,4 +107,6 @@ public interface IGbChannelService { void resetLevel(); + byte[] getTile(int z, int x, int y, String geoCoordSys); + } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java index 3f9bb323c..059cc59fd 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java @@ -1,5 +1,7 @@ package com.genersoft.iot.vmp.gb28181.service.impl; +import com.alibaba.excel.support.cglib.beans.BeanMap; +import com.alibaba.excel.util.BeanMapUtils; import com.genersoft.iot.vmp.common.enums.ChannelDataType; import com.genersoft.iot.vmp.conf.exception.ControllerException; import com.genersoft.iot.vmp.gb28181.bean.*; @@ -14,21 +16,24 @@ import com.genersoft.iot.vmp.gb28181.service.IGbChannelService; import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService; import com.genersoft.iot.vmp.service.bean.GPSMsgInfo; import com.genersoft.iot.vmp.streamPush.bean.StreamPush; +import com.genersoft.iot.vmp.utils.Coordtransform; import com.genersoft.iot.vmp.utils.DateUtil; import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import lombok.extern.slf4j.Slf4j; +import no.ecc.vectortile.VectorTileEncoder; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; @Slf4j @Service @@ -52,6 +57,8 @@ public class GbChannelServiceImpl implements IGbChannelService { @Autowired private GroupMapper groupMapper; + private final GeometryFactory geometryFactory = new GeometryFactory(); + @Override public CommonGBChannel queryByDeviceId(String gbDeviceId) { List commonGBChannels = commonGBChannelMapper.queryByDeviceId(gbDeviceId); @@ -859,4 +866,89 @@ public class GbChannelServiceImpl implements IGbChannelService { public void resetLevel() { commonGBChannelMapper.resetLevel(); } + + @Override + public byte[] getTile(int z, int x, int y, String geoCoordSys) { + double minLon = tile2lon(x, z); + double maxLon = tile2lon(x + 1, z); + double maxLat = tile2lat(y, z); + double minLat = tile2lat(y + 1, z); + + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.GCJ02ToWGS84(minLon, minLat); + minLon = minPosition[0]; + minLat = minPosition[1]; + + Double[] maxPosition = Coordtransform.GCJ02ToWGS84(maxLon, maxLat); + maxLon = maxPosition[0]; + maxLat = maxPosition[1]; + } + } + // 从数据库查询对应的数据 + List channelList = commonGBChannelMapper.queryCameraChannelInBox(minLon, maxLon, minLat, maxLat); + VectorTileEncoder encoder = new VectorTileEncoder(); + if (!channelList.isEmpty()) { + channelList.forEach(commonGBChannel -> { + double lon = commonGBChannel.getGbLongitude(); + double lat = commonGBChannel.getGbLatitude(); + // 转换为目标坐标系 + if (geoCoordSys != null) { + if (geoCoordSys.equalsIgnoreCase("GCJ02")) { + Double[] minPosition = Coordtransform.WGS84ToGCJ02(lon, lat); + lon = minPosition[0]; + lat = minPosition[1]; + } + } + + // 将 lon/lat 转为瓦片内像素坐标(0..256) + double[] px = lonLatToTilePixel(lon, lat, z, x, y); + Point pointGeom = geometryFactory.createPoint(new Coordinate(px[0], px[1])); + + BeanMap beanMap = BeanMapUtils.create(commonGBChannel); + +// Map props = new HashMap<>(); +// props.put("id", commonGBChannel.getGbId()); +// props.put("name", commonGBChannel.getGbName()); +// props.put("deviceId", commonGBChannel.getGbDeviceId()); +// props.put("status", commonGBChannel.getGbStatus()); + + encoder.addFeature("points", beanMap, pointGeom); + }); + } + return encoder.encode(); + } + + /** + * tile X/Z -> longitude (deg) + */ + private double tile2lon(int x, int z) { + double n = Math.pow(2.0, z); + return x / n * 360.0 - 180.0; + } + + /** + * tile Y/Z -> latitude (deg) + */ + private double tile2lat(int y, int z) { + double n = Math.pow(2.0, z); + double latRad = Math.atan(Math.sinh(Math.PI * (1 - 2.0 * y / n))); + return Math.toDegrees(latRad); + } + + /** + * lon/lat -> pixel in tile (0..256) + */ + private double[] lonLatToTilePixel(double lon, double lat, int z, int tileX, int tileY) { + double n = Math.pow(2.0, z); + double xtile = (lon + 180.0) / 360.0 * n; + + double latRad = Math.toRadians(lat); + double ytile = (1.0 - Math.log(Math.tan(latRad) + 1.0 / Math.cos(latRad)) / Math.PI) / 2.0 * n; + + double pixelX = (xtile - tileX) * 256.0; + double pixelY = (ytile - tileY) * 256.0; + + return new double[] { pixelX, pixelY }; + } } diff --git a/web/src/views/common/MapComponent.vue b/web/src/views/common/MapComponent.vue index 776052af4..a0c505aca 100755 --- a/web/src/views/common/MapComponent.vue +++ b/web/src/views/common/MapComponent.vue @@ -11,7 +11,9 @@ import VectorSource from 'ol/source/Vector' import Tile from 'ol/layer/Tile' import VectorLayer from 'ol/layer/Vector' import LayerGroup from 'ol/layer/Group' -import WebGLPointslayer from 'ol/layer/WebGLPoints' +import MVT from 'ol/format/MVT.js' +import VectorTileLayer from 'ol/layer/VectorTile.js' +import VectorTileSource from 'ol/source/VectorTile.js' import WebGLVectorLayer from 'ol/layer/WebGLVector' import Style from 'ol/style/Style' import Stroke from 'ol/style/Stroke' @@ -58,7 +60,6 @@ export default { init() { this.$store.dispatch('server/getMapConfig') .then(mapConfigList => { - console.log(mapConfigList.length) if (mapConfigList.length === 0) { if (window.mapParam.tilesUrl) { this.mapTileList.push({ @@ -95,6 +96,8 @@ export default { url: this.mapTileList[this.mapTileIndex].tilesUrl }) }) + console.log(4444) + console.log(this.mapTileList[this.mapTileIndex].tilesUrl) } else { tileLayer = new Tile({ preload: 4, @@ -145,6 +148,51 @@ export default { this.$emit('zoomChange', olMap.getView().getZoom()) }) }, + addVectorTileLayer(tileUrl){ + let source = new VectorTileSource({ + + format: new MVT(), + url: tileUrl + }) + let layer = new VectorTileLayer({ + source: source, + style: { + // 必须提供 style 配置,可以是对象或函数 + 'circle-radius': 4, + 'circle-fill-color': 'red', + 'circle-stroke-color': 'white', + 'circle-stroke-width': 0.5 + // 'icon-src': 'static/images/gis/sprite.png', + // 'icon-width': 120, + // 'icon-height': 40, + // 'icon-size': [40, 40], + // 'icon-anchor': [0.5, 1], + // 'icon-offset-origin': 'bottom-left', + // 'icon-offset': [ + // 'match', + // ['get', 'status'], + // 'ON', + // [0, 0], + // 'OFF', + // [40, 0], + // 'checked', + // [80, 0], + // [120, 60] + // ] + } + }) + olMap.addLayer(layer) + layer.on('click', (event) => { + console.log(event) + if (event.features.length > 0) { + const items = [] + for (let i = 0; i < event.features.length; i++) { + items.push(event.features[i].customData) + } + console.log(items) + } + }) + }, setCenter(point) { }, @@ -283,6 +331,8 @@ export default { return vectorLayer }, createPointLayer(data, clickEvent, option){ + console.log(444) + console.log(data) const features = [] let maxZoom = (option && option.maxZoom) ? option.maxZoom : olMap.getView().getMaxZoom() let minZoom = (option && option.minZoom) ? option.minZoom : olMap.getView().getMinZoom() @@ -333,7 +383,7 @@ export default { }) if (clickEvent && typeof clickEvent === 'function') { vectorLayer.on('click', (event) => { - + console.log(event) if (event.features.length > 0) { const items = [] for (let i = 0; i < event.features.length; i++) { @@ -414,6 +464,7 @@ export default { layer.getSource().addFeature(feature) }, updatePointLayer(layer, data, postponement) { + console.log(data) layer.getSource().clear(true) if (!data || data.length == 0) { return @@ -423,13 +474,9 @@ export default { const feature = new Feature(new Point(fromLonLat(data[i].position))) feature.setId(data[i].id) feature.customData = data[i].data - // const cloneStyle = new Style() - // cloneStyle.setImage(new Icon({ - // anchor: data[i].image.anchor, - // crossOrigin: 'Anonymous', - // src: data[i].image.src - // })) - // feature.setStyle(cloneStyle) + feature.setProperties({ + status: data[i].status + }) features.push(feature) } layer.getSource().addFeatures(features) diff --git a/web/src/views/map/index.vue b/web/src/views/map/index.vue index a4719697e..2af411763 100755 --- a/web/src/views/map/index.vue +++ b/web/src/views/map/index.vue @@ -5,7 +5,11 @@
- +
+ +