diff --git a/src/utils/api.js b/src/utils/api.js index 0a65fc4a..7e5e7b6d 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -358,7 +358,8 @@ export const api = { trend: apiVersion + '/locationIntelligence/active/trend', count: apiVersion + '/locationIntelligence/active/count', baseStation: apiVersion + '/locationIntelligence/baseStation', - followedSubscriber: apiVersion + '/locationIntelligence/followed/subscribers' + followedSubscriber: apiVersion + '/locationIntelligence/followed/subscribers', + tracking: apiVersion + '/locationIntelligence/trace/tracking' } } diff --git a/src/utils/constants.js b/src/utils/constants.js index 955f8c50..ce2d5094 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -46,7 +46,8 @@ export const storageKey = { userCustomizationConfig: 'userCustomizationConfig', linkInfo: 'cn-link-info', history: 'cn-history', - schemaEntityExplore: 'schema_entity_explore' + schemaEntityExplore: 'schema_entity_explore', + trackingSubscriberIds: 'tracking-subscriber-ids' } export const largeCountryList = ['CN', 'US', 'RU', 'AU', 'CA', 'KZ', 'IN', 'BR'] diff --git a/src/views/location/Index.vue b/src/views/location/Index.vue index e971c1ff..f9771a0e 100644 --- a/src/views/location/Index.vue +++ b/src/views/location/Index.vue @@ -4,6 +4,7 @@ +
@@ -23,62 +24,92 @@
+
+
-
-
{{$t('locationIntelligence.populationDensity')}}
-
-
-
-
-
-
{{legend.start}}~{{legend.end}}
-
{{legend.count}}
-
+
+
{{$t('locationIntelligence.populationDensity')}}
+
+
+
+
+
+
{{legend.start}}~{{legend.end}}
+
{{legend.count}}
-
-
{{$t('locationIntelligence.activeSubscribers')}}
-
-
{{activeCount}}
-
-{{valueToRangeValue(activeCountChain, unitTypes.percent).join(' ')}}
-
-
-
-
Followed Subscribers
-
- -
+
+
{{$t('locationIntelligence.activeSubscribers')}}
+
+
{{activeCount}}
+
-{{valueToRangeValue(activeCountChain, unitTypes.percent).join(' ')}}
+
+
+
+
Followed Subscribers
+
+ +
+
+ +
+
+ +
+
-
+
@@ -175,7 +206,7 @@ import maplibregl from 'maplibre-gl' import mapStyle from '@/views/charts2/charts/entityDetail/mapStyle' import 'maplibre-gl/dist/maplibre-gl.css' import { valueToRangeValue } from '@/utils/unit-convert' -import { unitTypes } from '@/utils/constants' +import { unitTypes, storageKey } from '@/utils/constants' import * as echarts from 'echarts' import { appListChartOption } from '@/views/charts2/charts/options/echartOption' import { pieOption } from '@/views/location/chartOption' @@ -202,76 +233,89 @@ export default { methods: { valueToRangeValue, async initMap () { - console.info(h3ToGeo('8931aa42cb7ffff')) const _this = this - const map = new maplibregl.Map({ - container: 'analysisMap', - style: mapStyle, - center: this.center, - maxZoom: this.maxZoom, - minZoom: this.minZoom, - zoom: this.defaultZoom - }) - maplibregl.addProtocol('cn', (params, callback) => { // 切片显示接口 防止跨域的问题 - fetch(`${params.url.split('://')[1]}`) - .then(t => { - if (t.status == 200) { - t.arrayBuffer().then(arr => { - callback(null, arr, null, null) - }) - } else { - callback(new Error(`Tile fetch error: ${t.statusText}`)) - } - }) - .catch(e => { - callback(new Error(e)) - }) - return { cancel: () => { } } - }) - this.mapChart = map + if (!this.mapChart) { + this.mapChart = new maplibregl.Map({ + container: 'analysisMap', + style: mapStyle, + center: this.center, + maxZoom: this.maxZoom, + minZoom: this.minZoom, + zoom: this.defaultZoom + }) + maplibregl.addProtocol('cn', (params, callback) => { // 切片显示接口 防止跨域的问题 + fetch(`${params.url.split('://')[1]}`) + .then(t => { + if (t.status == 200) { + t.arrayBuffer().then(arr => { + callback(null, arr, null, null) + }) + } else { + callback(new Error(`Tile fetch error: ${t.statusText}`)) + } + }) + .catch(e => { + callback(new Error(e)) + }) + return { cancel: () => { } } + }) + } - // 最先渲染右上角饼图 - await this.renderDensityPie() - // 然后渲染地图的色块、基站、人(包括右侧关注列表),最后渲染右上角折线图 - map.on('load', async function () { + this.mapChart.on('load', async function () { console.info('map loaded') - /* 地图色块 */ - _this.updateBoundaryBox() - const hexagonData = await _this.queryHexagon() - // 将查到的h3hexagon数据转为geojson - const polygonSourceData = _this.hexagonDataConverter(hexagonData) - map.addSource('hexGrid', { - type: 'geojson', - data: polygonSourceData - }) - // TODO 六边形边框,考虑加一层line layer - map.addLayer({ - id: 'hexagon', - type: 'fill', - source: 'hexGrid', - layout: {}, - paint: { - 'fill-color': ['get', 'color'], - 'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 1, 0.6] - } - }) - // 六边形的鼠标事件 - _this.bindHexagonEvents() - - /* 地图上的基站 */ + // 加载地图上的基站,基站不随tab的切换而改变 const baseStationData = await _this.queryBaseStation() _this.renderMarker(baseStationData, _this.tooltipType.baseStation) - /* 地图上的人 */ - const mapFollowedSubscriberData = await _this.queryMapFollowedSubscriber() - _this.renderMarker(mapFollowedSubscriberData, _this.tooltipType.human) - - /* 右侧关注列表 */ - - /* 右上角折线图 */ - await _this.renderActiveSubscribersLine() + if (_this.activeTab === 'locationMap') { + await _this.initLocationMapTab() + } else if (_this.activeTab === 'traceTracking') { + await _this.initTraceTrackingTab() + } }) }, + async initLocationMapTab () { + // 最先渲染右上角饼图 + await this.renderDensityPie() + // 然后渲染地图的色块、基站、人(包括右侧关注列表),最后渲染右上角折线图 + /* 地图色块 */ + this.updateBoundaryBox() + const hexagonData = await this.queryHexagon() + // 将查到的h3hexagon数据转为geojson + const polygonSourceData = this.hexagonDataConverter(hexagonData) + this.mapChart.addSource('hexGrid', { + type: 'geojson', + data: polygonSourceData + }) + // TODO 六边形边框,考虑加一层line layer + this.mapChart.addLayer({ + id: 'hexagon', + type: 'fill', + source: 'hexGrid', + layout: {}, + paint: { + 'fill-color': ['get', 'color'], + 'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 1, 0.6] + } + }) + // 六边形的鼠标事件 + this.bindHexagonEvents() + + /* 地图上的人 */ + const mapFollowedSubscriberData = await this.queryMapFollowedSubscriber() + this.renderMarker(mapFollowedSubscriberData, this.tooltipType.human) + + /* 右侧关注列表 */ + + /* 右上角折线图 */ + await this.renderActiveSubscribersLine() + }, + async initTraceTrackingTab () { + if (!this.currentShowSubscriber && this.trackingSubscribers.length > 0) { + this.currentShowSubscriber = { subscriberId: this.trackingSubscribers[0].subscriberId } + } + await this.queryTraceTracking() + }, async renderDensityPie () { const params = { ...this.timeFilter @@ -391,8 +435,6 @@ export default { }, async queryMapFollowedSubscriber () { this.loading.timeBarLoading = true - console.info(this.timeFilter) - console.info(this.minuteTimeFilter) try { const response = await axios.get(api.location.followedSubscriber, { params: this.minuteTimeFilter }) return response.data.data.list @@ -404,6 +446,26 @@ export default { } return [] }, + async queryTraceTracking () { + console.info(this.trackingSubscribers) + if (this.trackingSubscribers.length > 0) { + const params = { + ...this.timeFilter, + subscriberIds: this.trackingSubscribers.map(item => `'${item.subscriberId}'`).join(',') + } + try { + const response = await axios.get(api.location.tracking, { params }) + response.data.data.result.forEach(r => { + this.trackingSubscribers.find(item => item.subscriberId === r.subscriberId).trackRecords = r.trackRecords + }) + } catch (e) { + this.errorMsgHandler(e) + console.error(e) + } finally { + this.loading.timeBarLoading = false + } + } + }, renderMarker (data, type) { let svg if (type === this.tooltipType.baseStation) { @@ -416,39 +478,25 @@ export default { el.className = `map-marker map-marker--${type}` el.innerHTML = svg // 鼠标事件,控制tooltip显示和marker尺寸 - el.addEventListener('mouseenter', e => { - this.markerDom = el - if (!this.tooltip.mouseInMarkerOrTooltip) { - this.tooltip.x = e.clientX + 15 - e.offsetX - this.tooltip.y = e.clientY + 15 - e.offsetY - } - this.tooltip.mouseInMarkerOrTooltip = true - this.tooltip.type = this.tooltipType.baseStation - this.tooltip.showMarkerTooltip = true - el.classList.add('map-marker--hover') - }) - el.addEventListener('mouseleave', event => { - const tooltipDom = document.getElementById('tooltip') - if (!tooltipDom.contains(event.relatedTarget)) { - el.classList.remove('map-marker--hover') - this.tooltip.mouseInMarkerOrTooltip = false - this.tooltip.showMarkerTooltip = false - } - }) - - new maplibregl.Marker({ element: el }) + this.bindMarkerEvent(el) + const mapMarker = new maplibregl.Marker({ element: el }) .setLngLat([marker.longitude, marker.latitude]) .addTo(this.mapChart) + if (type === this.tooltipType.baseStation) { + this.baseStationMarkers.push(mapMarker) + } else if (type === this.tooltipType.human) { + this.humanMarkers.push(mapMarker) + } }) }, updateBoundaryBox () { const boundaryBox = this.mapChart.getBounds() - /*this.boundaryBox = { + /* this.boundaryBox = { maxLongitude: boundaryBox.getEast(), maxLatitude: boundaryBox.getNorth(), minLongitude: boundaryBox.getWest(), minLatitude: boundaryBox.getSouth() - }*/ + } */ this.boundaryBox = { maxLongitude: 140, maxLatitude: 50, @@ -459,17 +507,19 @@ export default { // 先使用min=0的等宽分组法,若后续出现特大或特小的异常值导致等宽分组效果不理想,考虑用分位数分组法 calculateValueRamp (data) { const max = _.maxBy(data, d => Number(d.number)) - const maxLength = String(max.number).length - const maxLegend = Math.ceil(max.number / Math.pow(10, maxLength - 1)) * Math.pow(10, maxLength - 1) const result = [] - for (let i = 1; i <= 5; i++) { - const item = { - start: maxLegend * (i - 1) / 5 + 1, - end: maxLegend * i / 5, - color: this.pieColorRamp[i - 1] + if (max) { + const maxLength = String(max.number).length + const maxLegend = Math.ceil(max.number / Math.pow(10, maxLength - 1)) * Math.pow(10, maxLength - 1) + for (let i = 1; i <= 5; i++) { + const item = { + start: maxLegend * (i - 1) / 5 + 1, + end: maxLegend * i / 5, + color: this.pieColorRamp[i - 1] + } + item.count = data.filter(d => d.number >= item.start && d.number < item.end).length + result.push(item) } - item.count = data.filter(d => d.number >= item.start && d.number < item.end).length - result.push(item) } return result }, @@ -502,47 +552,81 @@ export default { return [255, 255, 255] }, bindHexagonEvents () { + this.mapChart.on('mouseenter', 'hexagon', this.hexagonMouseEnter) + this.mapChart.on('mouseleave', 'hexagon', this.hexagonMouseLeave) + this.mapChart.on('mousemove', 'hexagon', this.hexagonMouseMove) + }, + unbindHexagonEvents () { + this.mapChart.off('mouseenter', this.hexagonMouseEnter) + this.mapChart.off('mouseleave', this.hexagonMouseLeave) + this.mapChart.off('mousemove', this.hexagonMouseMove) + }, + hexagonMouseEnter () { + this.tooltip.mouseIsInPolygon = true + }, + hexagonMouseLeave () { const _this = this - this.mapChart.on('mouseenter', 'hexagon', () => { - _this.tooltip.mouseIsInPolygon = true - }) - this.mapChart.on('mouseleave', 'hexagon', () => { - _this.tooltip.showPolygonTooltip = false - _this.tooltip.mouseIsInPolygon = false - // 去掉上一块的高亮 - hoverTrigger('hexGrid', _this.currentPolygon.id, false) - }) - this.mapChart.on('mousemove', 'hexagon', ({ point, originalEvent, features }) => { - if (!_this.tooltip.mouseInMarkerOrTooltip) { - _this.tooltip.showPolygonTooltip = true - _this.tooltip.type = _this.tooltipType.hexagon - if (_this.tooltip.type === _this.tooltipType.hexagon && _this.currentPolygon.id && _this.currentPolygon.id !== features[0].id) { - // 去掉上一块的高亮 - hoverTrigger('hexGrid', _this.currentPolygon.id, false) - } - _this.currentPolygon = features[0].properties - _this.currentPolygon.id = features[0].id - _this.tooltip.x = originalEvent.clientX + 15 - _this.tooltip.y = originalEvent.clientY + 5 + this.tooltip.showPolygonTooltip = false + this.tooltip.mouseIsInPolygon = false + // 去掉上一块的高亮 + hoverTrigger('hexGrid', this.currentPolygon.id, false) - // 鼠标滑过高亮 - hoverTrigger('hexGrid', _this.currentPolygon.id, true) - } - }) function hoverTrigger (source, id, hover) { _this.mapChart.setFeatureState({ source, id }, { hover }) } }, + hexagonMouseMove (e) { + const _this = this + const { originalEvent, features } = e + if (!this.tooltip.mouseInMarkerOrTooltip) { + this.tooltip.showPolygonTooltip = true + this.tooltip.type = this.tooltipType.hexagon + if (this.tooltip.type === this.tooltipType.hexagon && this.currentPolygon.id && this.currentPolygon.id !== features[0].id) { + // 去掉上一块的高亮 + hoverTrigger('hexGrid', this.currentPolygon.id, false) + } + this.currentPolygon = features[0].properties + this.currentPolygon.id = features[0].id + this.tooltip.x = originalEvent.clientX + 15 + this.tooltip.y = originalEvent.clientY + 5 + + // 鼠标滑过高亮 + hoverTrigger('hexGrid', this.currentPolygon.id, true) + } + + function hoverTrigger (source, id, hover) { + _this.mapChart.setFeatureState({ source, id }, { hover }) + } + }, + bindMarkerEvent (el) { + el.addEventListener('mouseenter', e => { + this.currentMarkerDom = el + if (!this.tooltip.mouseInMarkerOrTooltip) { + this.tooltip.x = e.clientX + 15 - e.offsetX + this.tooltip.y = e.clientY + 15 - e.offsetY + } + this.tooltip.mouseInMarkerOrTooltip = true + this.tooltip.type = this.tooltipType.baseStation + this.tooltip.showMarkerTooltip = true + el.classList.add('map-marker--hover') + }) + el.addEventListener('mouseleave', event => { + const tooltipDom = document.getElementById('tooltip') + if (!tooltipDom.contains(event.relatedTarget)) { + el.classList.remove('map-marker--hover') + this.tooltip.mouseInMarkerOrTooltip = false + this.tooltip.showMarkerTooltip = false + } + }) + }, tooltipMouseEnter () { this.tooltip.mouseInMarkerOrTooltip = true }, - tooltipMouseMove () { - }, tooltipMouseLeave (event) { - if (this.markerDom && !this.markerDom.contains(event.relatedTarget)) { + if (this.currentMarkerDom && !this.currentMarkerDom.contains(event.relatedTarget)) { this.tooltip.mouseInMarkerOrTooltip = false this.tooltip.showMarkerTooltip = false - this.markerDom.classList.remove('map-marker--hover') + this.currentMarkerDom.classList.remove('map-marker--hover') } }, reload (startTime, endTime, dateRangeValue) { @@ -573,8 +657,20 @@ export default { } }, watch: { - activeTab (n) { - + async activeTab (n) { + if (n === 'traceTracking') { + // 切换到轨迹追踪tab时,先移除地图上已有的图层和事件绑定、人型图标。基站予以保留 + this.unbindHexagonEvents() + this.mapChart.removeLayer('hexagon') + this.mapChart.removeSource('hexGrid') + this.humanMarkers.forEach(marker => { + marker.remove && marker.remove() + }) + this.humanMarkers = [] + await this.initTraceTrackingTab() + } else if (n === 'locationMap') { + await this.initLocationMapTab() + } } }, computed: { @@ -638,7 +734,10 @@ export default { const pieColorRamp = ['196,214,59', '190,230,255', '135,206,250', '63,133,186', '37,55,128'] const pieValueRamp = ref([]) const boundaryBox = ref({}) // minLongitude、maxLongitude、minLatitude、maxLatitude - const mapChart = shallowRef({}) + const mapChart = shallowRef(null) + const currentMarkerDom = shallowRef(null) + const humanMarkers = shallowRef([]) + const baseStationMarkers = shallowRef([]) const pieChart = shallowRef(null) const pieOption = ref({}) const lineChart = shallowRef(null) @@ -646,6 +745,17 @@ export default { const currentBaseStation = ref({}) const currentSubscriber = ref({}) const currentPolygon = ref({}) + + // 从localStorage中获取数据 + const trackingSubscribers = ref([]) + localStorage.getItem(storageKey.trackingSubscriberIds) && (trackingSubscribers.value = JSON.parse(localStorage.getItem(storageKey.trackingSubscriberIds)).map(id => ({ subscriberId: id }))) + const test = ['gary6411', 'test6431', 'test6430', 'test6422'] + test.forEach(id => { + trackingSubscribers.value.push({ subscriberId: id }) + }) + console.info(trackingSubscribers) + + const currentShowSubscriber = ref(null) const loading = ref({ mapLoading: true, // mapLoading控制地图的loading,它状态同时受hexagonLoading、timeBarLoading、baseStationLoading影响 hexagonLoading: true, // 六边形加载状态 @@ -665,7 +775,9 @@ export default { pieValueRamp, // 饼图数值坡度,动态获取 boundaryBox, // 查六边形的范围,minLongitude、maxLongitude、minLatitude、maxLatitude mapChart, // 地图对象 - markerDom: shallowRef(null), // 记录当前鼠标悬停的marker的dom + currentMarkerDom, // 记录当前鼠标悬停的marker的dom + humanMarkers, // 储存人marker的引用 + baseStationMarkers, // 储存基站marker的引用 pieChart, // 饼图对象 pieOption, lineChart, // 折线图对象 @@ -673,6 +785,8 @@ export default { currentBaseStation, // 鼠标当前悬浮的基站 currentSubscriber, // 鼠标当前悬浮的Subscriber currentPolygon, // 鼠标当前悬浮的六边形 + trackingSubscribers, // 存放当前追踪的Subscriber列表 + currentShowSubscriber, // 当前在地图上展示轨迹的Subscriber TODO 从url获取 loading, // 控制组件内各处loading图标 maxZoom: 14, // 地图最小缩放比例 minZoom: 3, // 地图最大缩放比例