From e7a44df9348c024429bc504140d027dd4dccedbd Mon Sep 17 00:00:00 2001 From: xiongjj Date: Tue, 22 Oct 2024 22:32:49 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90feature=E3=80=91=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=94=AF=E6=8C=81=E5=A4=9A=E9=80=89;=20revie?= =?UTF-8?q?wby=20luox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- babel.config.js | 1 + package.json | 1 + src/common/table-popup/TablePopup.vue | 5 + src/mapboxgl/_utils/HightlighLayer.ts | 318 ---------- .../_utils/__tests__/HighlightLayer.spec.js | 150 ----- src/mapboxgl/components.ts | 2 + src/mapboxgl/entrys.json | 1 + .../layer-highlight/LayerHighlight.vue | 205 ++++++ .../LayerHighlightViewModel.ts | 587 ++++++++++++++++++ .../__tests__/LayerHighlightViewModel.spec.js | 225 +++++++ .../__tests__/LayersHighlight.spec.js | 201 ++++++ src/mapboxgl/layer-highlight/index.js | 9 + src/mapboxgl/layer-highlight/style/index.js | 4 + .../style/layer-highlight.scss | 14 + src/mapboxgl/map-popup/MapPopup.vue | 17 +- src/mapboxgl/query/Query.vue | 261 +++++--- src/mapboxgl/query/QueryViewModel.js | 129 +--- src/mapboxgl/query/__tests__/Query.spec.js | 77 ++- .../query/__tests__/QueryViewModel.spec.js | 116 ---- src/mapboxgl/query/style/query.scss | 8 +- src/mapboxgl/style.scss | 1 + .../web-map/control/identify/Identify.vue | 329 +--------- .../control/identify/IdentifyViewModel.js | 41 -- .../identify/__tests__/Identify.spec.js | 367 ++--------- .../control/identify/style/identify.scss | 47 +- test/unit/mocks/map.js | 1 + test/unit/mocks/services.js | 25 + 27 files changed, 1621 insertions(+), 1521 deletions(-) delete mode 100644 src/mapboxgl/_utils/HightlighLayer.ts delete mode 100644 src/mapboxgl/_utils/__tests__/HighlightLayer.spec.js create mode 100644 src/mapboxgl/layer-highlight/LayerHighlight.vue create mode 100644 src/mapboxgl/layer-highlight/LayerHighlightViewModel.ts create mode 100644 src/mapboxgl/layer-highlight/__tests__/LayerHighlightViewModel.spec.js create mode 100644 src/mapboxgl/layer-highlight/__tests__/LayersHighlight.spec.js create mode 100644 src/mapboxgl/layer-highlight/index.js create mode 100644 src/mapboxgl/layer-highlight/style/index.js create mode 100644 src/mapboxgl/layer-highlight/style/layer-highlight.scss delete mode 100644 src/mapboxgl/query/__tests__/QueryViewModel.spec.js delete mode 100644 src/mapboxgl/web-map/control/identify/IdentifyViewModel.js diff --git a/babel.config.js b/babel.config.js index 15cd3484d..09cad4981 100644 --- a/babel.config.js +++ b/babel.config.js @@ -15,6 +15,7 @@ module.exports = function (api) { ]; const plugins = [ '@babel/plugin-transform-runtime', + 'transform-vue-jsx', 'transform-flow-strip-types', '@babel/plugin-transform-modules-commonjs', [ diff --git a/package.json b/package.json index 0385768d6..83670e3a6 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "babel-loader": "^8.0.5", "babel-plugin-component": "^1.1.1", "babel-plugin-transform-flow-strip-types": "^6.14.0", + "babel-plugin-transform-vue-jsx": "^3.7.0", "babel-register": "^6.22.0", "chalk": "^2.4.2", "chromedriver": "^2.46.0", diff --git a/src/common/table-popup/TablePopup.vue b/src/common/table-popup/TablePopup.vue index 25bb48048..910b0cd3d 100644 --- a/src/common/table-popup/TablePopup.vue +++ b/src/common/table-popup/TablePopup.vue @@ -9,6 +9,7 @@ :columns="columns" :rowKey="(record, index) => index" :pagination="false" + :showHeader="showHeader" /> @@ -39,6 +40,10 @@ export default { splitLine: { type: Boolean, default: true + }, + showHeader: { + type: Boolean, + default: true } }, watch: { diff --git a/src/mapboxgl/_utils/HightlighLayer.ts b/src/mapboxgl/_utils/HightlighLayer.ts deleted file mode 100644 index 8be4ab20c..000000000 --- a/src/mapboxgl/_utils/HightlighLayer.ts +++ /dev/null @@ -1,318 +0,0 @@ -import mapboxgl from 'vue-iclient/static/libs/mapboxgl/mapbox-gl-enhance'; -import CircleStyle from 'vue-iclient/src/mapboxgl/_types/CircleStyle'; -import LineStyle from 'vue-iclient/src/mapboxgl/_types/LineStyle'; -import FillStyle from 'vue-iclient/src/mapboxgl/_types/FillStyle'; - -interface HighlightStyle { - circle: InstanceType; - line: InstanceType; - fill: InstanceType; - strokeLine?: InstanceType; - stokeLine?: InstanceType; -} - -interface HighlightLayerOptions { - name: string; - style: HighlightStyle; - layerIds?: string[]; - fields?: string[]; - filter?: any[]; - clickTolerance?: number; -} - -type StyleTypes = Array; - -type BasicStyleAttrs = { - [prop in StyleTypes[number]]?: string[]; -}; - -type LayerClickedFeature = mapboxglTypes.MapboxGeoJSONFeature & { - _vectorTileFeature: { - _keys: string[]; - [prop: string]: any; - }; -}; - -const HIGHLIGHT_COLOR = '#01ffff'; - -const PAINT_BASIC_ATTRS: BasicStyleAttrs = { - circle: ['circle-radius', 'circle-stroke-width'], - line: ['line-width'], - strokeLine: ['line-width'] -}; -const PAINT_DEFAULT_STYLE = { - 'circle-radius': 8, - 'circle-stroke-width': 2, - 'line-width': 2 -}; - -const LAYER_DEFAULT_STYLE = { - circle: { - paint: { - 'circle-color': HIGHLIGHT_COLOR, - 'circle-opacity': 0.6, - 'circle-stroke-color': HIGHLIGHT_COLOR, - 'circle-stroke-opacity': 1 - }, - layout: { - visibility: 'visible' - } - }, - line: { - paint: { - 'line-color': HIGHLIGHT_COLOR, - 'line-opacity': 1 - }, - layout: { - visibility: 'visible' - } - }, - fill: { - paint: { - 'fill-color': HIGHLIGHT_COLOR, - 'fill-opacity': 0.6, - 'fill-outline-color': HIGHLIGHT_COLOR - }, - layout: { - visibility: 'visible' - } - }, - symbol: { - layout: { - 'icon-size': 5 - } - }, - strokeLine: { - paint: { - 'line-width': 3, - 'line-color': HIGHLIGHT_COLOR, - 'line-opacity': 1 - }, - layout: { - visibility: 'visible' - } - } -}; - -const HIGHLIGHT_DEFAULT_STYLE: HighlightStyle = { - circle: new CircleStyle(), - line: new LineStyle(), - fill: new FillStyle(), - strokeLine: new LineStyle() -}; - -export default class HighlightLayer extends mapboxgl.Evented { - uniqueName: string; - targetLayerIds: string[] = []; - hightlightStyle: HighlightStyle; - filterExp?: any[]; - filterFields?: string[]; - clickTolerance = 5; - map: mapboxglTypes.Map; - fire: (type: string, params?: any) => void; - - constructor(options: HighlightLayerOptions) { - super(); - this.uniqueName = options.name; - this.hightlightStyle = this._transHighlightStyle(options.style); - this.targetLayerIds = options.layerIds || []; - this.filterExp = options.filter; - this.filterFields = options.fields; - this.clickTolerance = options.clickTolerance ?? 5; - this._handleMapClick = this._handleMapClick.bind(this); - this._handleMapMouseEnter = this._handleMapMouseEnter.bind(this); - this._handleMapMouseLeave = this._handleMapMouseLeave.bind(this); - } - - setHighlightStyle(style: HighlightStyle) { - this.hightlightStyle = this._transHighlightStyle(style); - } - - setTargetLayers(layerIds: string[]) { - this.targetLayerIds = layerIds; - } - - setFilterExp(exp: any[]) { - this.filterExp = exp; - } - - setFilterFields(fields: string[]) { - this.filterFields = fields; - } - - registerMapClick() { - this.map.on('click', this._handleMapClick); - } - - unregisterMapClick() { - this.map.off('click', this._handleMapClick); - } - - registerLayerMouseEvents(layerIds: string[]) { - this.setTargetLayers(layerIds); - layerIds.forEach(layerId => { - this.map.on('mousemove', layerId, this._handleMapMouseEnter); - this.map.on('mouseleave', layerId, this._handleMapMouseLeave); - }); - } - - unregisterLayerMouseEvents() { - this.targetLayerIds.forEach(layerId => { - this.map.off('mousemove', layerId, this._handleMapMouseEnter); - this.map.off('mouseleave', layerId, this._handleMapMouseLeave); - }); - } - - addHighlightLayers(layer: mapboxglTypes.Layer, filter: any[] = this.filterExp) { - let type = layer.type as unknown as StyleTypes[number]; - let paint = layer.paint; - const id = layer.id; - // 如果是面的strokline,处理成面 - if (id.includes('-strokeLine') && type === 'line') { - type = 'fill'; - paint = {}; - } - const types = [type] as unknown as StyleTypes; - if (type === 'fill') { - types.push('strokeLine'); - } - const layerHighlightStyle = this._createLayerHighlightStyle(types, id); - if (['circle', 'line', 'fill'].includes(type)) { - const layerStyle = layerHighlightStyle[type]; - const highlightLayer = Object.assign({}, layer, { - id: this._createHightlightLayerId(id), - type, - paint: Object.assign({}, paint, LAYER_DEFAULT_STYLE[type].paint, layerStyle?.paint), - layout: Object.assign({}, LAYER_DEFAULT_STYLE[type].layout, layerStyle?.layout), - filter - }); - this.map.addLayer(highlightLayer as mapboxglTypes.AnyLayer); - this.targetLayerIds.push(id); - this.targetLayerIds = this._uniqueLayerIds(this.targetLayerIds); - } - if (type === 'fill') { - const layerStyle = layerHighlightStyle.strokeLine; - const highlightLayer = Object.assign({}, layer, { - id: this._createHighlightStrokeLayerId(id), - type: 'line', - paint: Object.assign({}, LAYER_DEFAULT_STYLE['strokeLine'].paint, layerStyle?.paint), - layout: Object.assign({}, LAYER_DEFAULT_STYLE['strokeLine'].layout, layerStyle?.layout), - filter - }); - this.map.addLayer(highlightLayer as mapboxglTypes.AnyLayer); - } - } - - removeHighlightLayers(layerIds: string[] = []) { - if (!this.map) { - return; - } - const layersToRemove = this._getHighlightLayerIds(this._uniqueLayerIds(this.targetLayerIds.concat(layerIds))); - layersToRemove.forEach(layerId => { - if (this.map.getLayer(layerId)) { - this.map.removeLayer(layerId); - } - }); - } - - createFilterExp(feature: LayerClickedFeature, fields: string[] = this.filterFields) { - // 高亮过滤(所有字段) - const filterKeys = ['smx', 'smy', 'lon', 'lat', 'longitude', 'latitude', 'x', 'y', 'usestyle', 'featureinfo']; - const isBasicType = (item: any) => { - return typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'; - }; - const filter: any[] = ['all']; - const featureKeys: string[] = fields || feature._vectorTileFeature._keys; - return featureKeys.reduce((exp, key) => { - if (filterKeys.indexOf(key.toLowerCase()) === -1 && isBasicType(feature.properties[key])) { - exp.push(['==', key, feature.properties[key]]); - } - return exp; - }, filter); - } - - _createLayerHighlightStyle(types: StyleTypes, layerId: string) { - const highlightStyle: HighlightStyle = JSON.parse(JSON.stringify(this.hightlightStyle)); - types - .filter(type => PAINT_BASIC_ATTRS[type]) - .forEach(type => { - if (!highlightStyle[type]) { - // @ts-ignore - highlightStyle[type] = HIGHLIGHT_DEFAULT_STYLE[type]; - } - const paintBasicAttrs = PAINT_BASIC_ATTRS[type]; - paintBasicAttrs.forEach(paintType => { - if (!highlightStyle[type].paint?.[paintType]) { - const originPaintValue = - type !== 'strokeLine' && this.map.getLayer(layerId) && this.map.getPaintProperty(layerId, paintType); - highlightStyle[type].paint = Object.assign({}, highlightStyle[type].paint, { - [paintType]: originPaintValue || PAINT_DEFAULT_STYLE[paintType] - }); - } - }); - }); - return highlightStyle; - } - - _transHighlightStyle(highlightStyle: HighlightStyle) { - const nextHighlightStyle = JSON.parse(JSON.stringify(highlightStyle)); - // 兼容 strokeLine 错误写法 stokeLine - if ('stokeLine' in highlightStyle && !('strokeLine' in highlightStyle)) { - nextHighlightStyle.strokeLine = highlightStyle.stokeLine; - delete nextHighlightStyle.stokeLine; - } - return nextHighlightStyle; - } - - _getHighlightLayerIds(layerIds: string[]) { - return layerIds.reduce((idList, layerId) => { - const highlightLayerId = this._createHightlightLayerId(layerId); - const highlightStrokeLayerId = this._createHighlightStrokeLayerId(layerId); - idList.push(highlightLayerId, highlightStrokeLayerId); - return idList; - }, []); - } - - _uniqueLayerIds(layerIds: string[]) { - return Array.from(new Set(layerIds)); - } - - _createHightlightLayerId(layerId: string) { - return `${layerId}-${this.uniqueName}-SM-highlighted`; - } - - _createHighlightStrokeLayerId(layerId: string) { - const highlightLayerId = this._createHightlightLayerId(layerId); - return `${highlightLayerId}-StrokeLine`; - } - - _handleMapClick(e: mapboxglTypes.MapLayerMouseEvent) { - const features = this._queryLayerFeatures(e); - this.removeHighlightLayers(); - if (features[0]?.layer) { - this.addHighlightLayers(features[0].layer, this.filterExp ?? this.createFilterExp(features[0])); - } - this.fire('mapselectionchanged', { features }); - } - - _handleMapMouseEnter() { - this.map.getCanvas().style.cursor = 'pointer'; - } - - _handleMapMouseLeave() { - this.map.getCanvas().style.cursor = ''; - } - - _queryLayerFeatures(e: mapboxglTypes.MapLayerMouseEvent) { - const map = e.target; - const layersOnMap = this.targetLayerIds.filter(item => !!this.map.getLayer(item)); - const bbox = [ - [e.point.x - this.clickTolerance, e.point.y - this.clickTolerance], - [e.point.x + this.clickTolerance, e.point.y + this.clickTolerance] - ] as unknown as [mapboxglTypes.PointLike, mapboxglTypes.PointLike]; - const features = map.queryRenderedFeatures(bbox, { - layers: layersOnMap - }) as unknown as LayerClickedFeature[]; - return features; - } -} diff --git a/src/mapboxgl/_utils/__tests__/HighlightLayer.spec.js b/src/mapboxgl/_utils/__tests__/HighlightLayer.spec.js deleted file mode 100644 index 6411a8447..000000000 --- a/src/mapboxgl/_utils/__tests__/HighlightLayer.spec.js +++ /dev/null @@ -1,150 +0,0 @@ -import HighlightLayer from '../HightlighLayer'; -import Map from '@mocks/map'; - -describe('HighlightLayer', () => { - const highlightStyle = { - line: { - paint: { - 'line-width': 3, - 'line-color': '#01ffff', - 'line-opacity': 1 - } - }, - circle: { - paint: { - 'circle-color': '#01ffff', - 'circle-opacity': 0.6, - 'circle-radius': 8, - 'circle-stroke-width': 2, - 'circle-stroke-color': '#01ffff', - 'circle-stroke-opacity': 1 - } - }, - fill: { - paint: { - 'fill-color': '#01ffff', - 'fill-opacity': 0.6, - 'fill-outline-color': '#01ffff' - } - }, - stokeLine: { - paint: { - 'line-width': 3, - 'line-color': '#01ffff', - 'line-opacity': 1 - } - } - }; - - let map; - const uniqueName = 'Test'; - let viewModel; - - beforeEach(() => { - map = new Map({ - style: { center: [0, 0], zoom: 1, layers: [], sources: {} } - }); - viewModel = new HighlightLayer({ name: uniqueName, style: highlightStyle }); - viewModel.map = map; - }); - - it('toogle show highlight layers', done => { - const pointLayer = { - id: 'pointLayer', - type: 'circle' - }; - viewModel.addHighlightLayers(pointLayer); - const layers = map.getStyle().layers; - expect(layers.length).toBe(1); - expect(layers[0].id).toBe(`${pointLayer.id}-${uniqueName}-SM-highlighted`); - expect(viewModel.targetLayerIds).toEqual([pointLayer.id]); - viewModel.removeHighlightLayers(); - expect(map.getStyle().layers.length).toBe(0); - expect(viewModel.targetLayerIds).toEqual([pointLayer.id]); - viewModel.setTargetLayers([]); - expect(viewModel.targetLayerIds).toEqual([]); - done(); - }); - - it('map click target layer by specified filterExp', done => { - viewModel.registerMapClick(); - viewModel.once('mapselectionchanged', ({ features }) => { - expect(features.length).toBeGreaterThan(0); - const layers = map.getStyle().layers; - const mockLayerName = 'China'; - expect(layers.length).toBe(2); - expect(layers[0].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted`); - expect(layers[1].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted-StrokeLine`); - expect(layers[0].filter).toEqual(viewModel.filterExp); - expect(layers[1].filter).toEqual(viewModel.filterExp); - expect(viewModel.targetLayerIds).toEqual([mockLayerName]); - viewModel.removeHighlightLayers(); - expect(map.getStyle().layers.length).toBe(0); - expect(viewModel.targetLayerIds).toEqual([mockLayerName]); - viewModel.unregisterMapClick(); - done(); - }); - expect(viewModel.filterExp).toBeUndefined(); - const filterExp = ['all', ['==', 'key1', 'value1']]; - viewModel.setFilterExp(filterExp); - expect(viewModel.filterExp).toEqual(filterExp); - viewModel.map.fire('click', { target: map, point: { x: 10, y: 5 } }); - }); - - it('map click target layer by specified filterFields', done => { - viewModel.registerMapClick(); - viewModel.once('mapselectionchanged', ({ features }) => { - expect(features.length).toBeGreaterThan(0); - const layers = map.getStyle().layers; - const mockLayerName = 'China'; - expect(layers.length).toBe(2); - expect(layers[0].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted`); - expect(layers[1].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted-StrokeLine`); - const filterExp = viewModel.createFilterExp(features[0], viewModel.filterFields); - expect(layers[0].filter).toEqual(filterExp); - expect(layers[1].filter).toEqual(filterExp); - expect(viewModel.targetLayerIds).toEqual([mockLayerName]); - viewModel.removeHighlightLayers(); - expect(map.getStyle().layers.length).toBe(0); - expect(viewModel.targetLayerIds).toEqual([mockLayerName]); - viewModel.unregisterMapClick(); - done(); - }); - expect(viewModel.filterFields).toBeUndefined(); - const filterFields = ['title', 'subtitle', 'imgUrl', 'description', 'index']; - viewModel.setFilterFields(filterFields); - expect(viewModel.filterFields).toEqual(filterFields); - viewModel.map.fire('click', { target: map, point: { x: 10, y: 5 } }); - }); - - it('map click empty', done => { - viewModel.registerMapClick(); - viewModel.once('mapselectionchanged', ({ features }) => { - expect(features.length).toBe(0); - viewModel.unregisterMapClick(); - done(); - }); - viewModel.map.fire('click', { target: map, point: { x: 0, y: 5 } }); - }); - - it('map canvas style', done => { - const canvaStyle = {}; - const events = {}; - jest.spyOn(map, 'getCanvas').mockImplementation(() => ({ - style: canvaStyle - })); - jest.spyOn(map, 'on').mockImplementation((type, layerId, cb) => { - events[type] = cb ? cb : layerId; - }); - const targetIds = ['Test1']; - viewModel.registerLayerMouseEvents(targetIds); - events.mousemove(); - expect(viewModel.targetLayerIds).toEqual(targetIds); - expect(canvaStyle.cursor).toBe('pointer'); - events.mouseleave(); - expect(canvaStyle.cursor).toBe(''); - viewModel.unregisterLayerMouseEvents(); - done(); - }); -}); - diff --git a/src/mapboxgl/components.ts b/src/mapboxgl/components.ts index 713d072a1..d2ab42801 100644 --- a/src/mapboxgl/components.ts +++ b/src/mapboxgl/components.ts @@ -109,6 +109,7 @@ import { default as VideoPlusPopup } from 'vue-iclient/src/mapboxgl/video-plus/u import { default as VideoPlusDraw } from 'vue-iclient/src/mapboxgl/video-plus/control/draw/index'; import { default as GraphMap } from 'vue-iclient/src/mapboxgl/graph-map'; import { default as MapPopup } from 'vue-iclient/src/mapboxgl/map-popup/index.js'; +import { default as HighlightLayer } from 'vue-iclient/src/mapboxgl/layer-highlight/index.js'; const components = { Avatar, @@ -192,6 +193,7 @@ const components = { FlyTo, Identify, MapPopup, + HighlightLayer, LayerColor, LayerList, LayerManager, diff --git a/src/mapboxgl/entrys.json b/src/mapboxgl/entrys.json index eb61ee1ac..bd0c55197 100644 --- a/src/mapboxgl/entrys.json +++ b/src/mapboxgl/entrys.json @@ -79,6 +79,7 @@ "draw": "vue-iclient/src/mapboxgl/web-map/control/draw/index.js", "fly-to": "vue-iclient/src/mapboxgl/web-map/control/fly-to/index.js", "map-popup": "vue-iclient/src/mapboxgl/map-popup/index.js", + "layer-highlight": "vue-iclient/src/mapboxgl/layer-highlight/index.js", "identify": "vue-iclient/src/mapboxgl/web-map/control/identify/index.js", "layer-color": "vue-iclient/src/mapboxgl/web-map/control/layer-color/index.js", "layer-list": "vue-iclient/src/mapboxgl/web-map/control/layer-list/index.js", diff --git a/src/mapboxgl/layer-highlight/LayerHighlight.vue b/src/mapboxgl/layer-highlight/LayerHighlight.vue new file mode 100644 index 000000000..eca317d73 --- /dev/null +++ b/src/mapboxgl/layer-highlight/LayerHighlight.vue @@ -0,0 +1,205 @@ + + + + diff --git a/src/mapboxgl/layer-highlight/LayerHighlightViewModel.ts b/src/mapboxgl/layer-highlight/LayerHighlightViewModel.ts new file mode 100644 index 000000000..7426b5a5e --- /dev/null +++ b/src/mapboxgl/layer-highlight/LayerHighlightViewModel.ts @@ -0,0 +1,587 @@ +import mapboxgl from 'vue-iclient/static/libs/mapboxgl/mapbox-gl-enhance'; +import CircleStyle from 'vue-iclient/src/mapboxgl/_types/CircleStyle'; +import LineStyle from 'vue-iclient/src/mapboxgl/_types/LineStyle'; +import FillStyle from 'vue-iclient/src/mapboxgl/_types/FillStyle'; +import { getFeatureCenter, getValueCaseInsensitive } from 'vue-iclient/src/common/_utils/util'; + +interface HighlightStyle { + circle: InstanceType; + line: InstanceType; + fill: InstanceType; + strokeLine?: InstanceType; + stokeLine?: InstanceType; +} + +interface FieldsDisplayInfo { + field: string; + title: string; + slotName?: string; +} + +interface LayerEventCursorMap { + mousemove: string; + mouseleave: string; +} + +interface HighlightLayerOptions { + name: string; + layerIds?: string[]; + style: HighlightStyle; + featureFieldsMap?: Record; + displayFieldsMap?: Record; + filter?: any[]; + clickTolerance?: number; + multiSelection?: boolean; + eventsCursor?: LayerEventCursorMap; +} + +type StyleTypes = Array; + +type BasicStyleAttrs = { + [prop in StyleTypes[number]]?: string[]; +}; + +type LayerClickedFeature = mapboxglTypes.MapboxGeoJSONFeature & { + geometry: Exclude; + _vectorTileFeature?: { + _keys: string[]; + [prop: string]: any; + }; +}; + +interface PopupFieldsInfo { + attribute: string; + attributeValue: string; + alias?: string; + slotName?: string; +} + +interface PopupFeatureInfo { + coordinates: LayerClickedFeature['geometry']['coordinates']; + info: PopupFieldsInfo[]; +} + +interface MapLoadInfo { + map: mapboxglTypes.Map; + [prop: string]: any; +} + +enum DataSelectorMode { + SINGLE = 'SINGLE', // 单选 + MULTIPLE = 'MULTIPLE', // 多选 + ALL = 'ALL' // 全选 +} + +interface MapSelectionChangedEmit { + features: LayerClickedFeature[]; + popupInfos: PopupFeatureInfo['info'][]; + lnglats: PopupFeatureInfo['coordinates'][]; + highlightLayerIds: string[]; + dataSelectorMode: DataSelectorMode; +} + +interface CreateFilterExpParams { + feature: LayerClickedFeature; + targetId?: string; + fields?: string[]; +} + +interface UpdateHighlightOptions { + layerId: string; + features: LayerClickedFeature[]; +} + +interface CreateRelatedDatasParams { + features: LayerClickedFeature[]; + targetId: string; + isMultiple?: boolean; +} + +const HIGHLIGHT_COLOR = '#01ffff'; + +const PAINT_BASIC_ATTRS: BasicStyleAttrs = { + circle: ['circle-radius', 'circle-stroke-width'], + line: ['line-width'], + strokeLine: ['line-width'] +}; +const PAINT_DEFAULT_STYLE = { + 'circle-radius': 8, + 'circle-stroke-width': 2, + 'line-width': 2 +}; + +const LAYER_DEFAULT_STYLE = { + circle: { + paint: { + 'circle-color': HIGHLIGHT_COLOR, + 'circle-opacity': 0.6, + 'circle-stroke-color': HIGHLIGHT_COLOR, + 'circle-stroke-opacity': 1 + }, + layout: { + visibility: 'visible' + } + }, + line: { + paint: { + 'line-color': HIGHLIGHT_COLOR, + 'line-opacity': 1 + }, + layout: { + visibility: 'visible' + } + }, + fill: { + paint: { + 'fill-color': HIGHLIGHT_COLOR, + 'fill-opacity': 0.6, + 'fill-outline-color': HIGHLIGHT_COLOR + }, + layout: { + visibility: 'visible' + } + }, + symbol: { + layout: { + 'icon-size': 5 + } + }, + strokeLine: { + paint: { + 'line-width': 3, + 'line-color': HIGHLIGHT_COLOR, + 'line-opacity': 1 + }, + layout: { + visibility: 'visible' + } + } +}; + +const HIGHLIGHT_DEFAULT_STYLE: HighlightStyle = { + circle: new CircleStyle(), + line: new LineStyle(), + fill: new FillStyle(), + strokeLine: new LineStyle() +}; + +export default class HighlightLayer extends mapboxgl.Evented { + private dataSelectorMode: DataSelectorMode = DataSelectorMode.SINGLE; + private activeTargetId: string | null = null; + private resultFeatures: LayerClickedFeature[] = []; + highlightOptions: HighlightLayerOptions; + map: mapboxglTypes.Map; + fire: (type: string, params?: any) => void; + + constructor(options: HighlightLayerOptions) { + super(); + this.handleMapClick = this.handleMapClick.bind(this); + this.handleMapMouseEnter = this.handleMapMouseEnter.bind(this); + this.handleMapMouseLeave = this.handleMapMouseLeave.bind(this); + this.handleLayerKeydown = this.handleLayerKeydown.bind(this); + this.handleLayerKeyup = this.handleLayerKeyup.bind(this); + + this.highlightOptions = { + ...options, + style: this.transHighlightStyle(options.style), + layerIds: (options.layerIds ?? []).slice(), + featureFieldsMap: options.featureFieldsMap, + displayFieldsMap: options.displayFieldsMap, + clickTolerance: options.clickTolerance ?? 5, + multiSelection: options.multiSelection ?? false + }; + } + + setMap({ map }: MapLoadInfo) { + this.map = map; + this.registerMapClick(); + this.setTargetLayers(this.highlightOptions.layerIds); + } + + setHighlightStyle(style: HighlightStyle) { + this.highlightOptions.style = this.transHighlightStyle(style); + } + + setTargetLayers(layerIds: string[]) { + this.unregisterLayerMouseEvents(); + this.registerLayerMouseEvents(layerIds); + this.unregisterLayerMultiClick(); + this.registerLayerMultiClick(); + this.highlightOptions.layerIds = layerIds; + } + + setFeatureFieldsMap(fieldsMap: Record) { + this.highlightOptions.featureFieldsMap = fieldsMap; + } + + setDisplayFieldsMap(fieldsMap: Record) { + this.highlightOptions.displayFieldsMap = fieldsMap; + } + + setMultiSelection(multiSelection: boolean) { + this.highlightOptions.multiSelection = multiSelection; + this.unregisterLayerMultiClick(); + this.registerLayerMultiClick(); + } + + setClickTolerance(clickTolerance: number) { + this.highlightOptions.clickTolerance = clickTolerance; + } + + registerMapClick() { + if (!this.map) { + return; + } + this.map.on('click', this.handleMapClick); + } + + unregisterMapClick() { + if (!this.map) { + return; + } + this.map.off('click', this.handleMapClick); + } + + registerLayerMultiClick() { + if (this.highlightOptions.multiSelection) { + window.addEventListener('keydown', this.handleLayerKeydown); + window.addEventListener('keyup', this.handleLayerKeyup); + } + } + + unregisterLayerMultiClick() { + window.removeEventListener('keydown', this.handleLayerKeydown); + window.removeEventListener('keyup', this.handleLayerKeyup); + } + + registerLayerMouseEvents(layerIds: string[]) { + if (!layerIds?.length || !this.map) { + return; + } + layerIds.forEach(layerId => { + this.map.on('mousemove', layerId, this.handleMapMouseEnter); + this.map.on('mouseleave', layerId, this.handleMapMouseLeave); + }); + } + + unregisterLayerMouseEvents() { + if (!this.map) { + return; + } + this.highlightOptions.layerIds.forEach(layerId => { + this.map.off('mousemove', layerId, this.handleMapMouseEnter); + this.map.off('mouseleave', layerId, this.handleMapMouseLeave); + }); + } + + addHighlightLayers(layer: mapboxglTypes.Layer, filter: any) { + let type = layer.type as unknown as StyleTypes[number]; + let paint = layer.paint; + const id = layer.id; + // 如果是面的strokline,处理成面 + if (id.includes('-strokeLine') && type === 'line') { + type = 'fill'; + paint = {}; + } + const types = [type] as unknown as StyleTypes; + if (type === 'fill') { + types.push('strokeLine'); + } + const layerHighlightStyle = this.createLayerHighlightStyle(types, id); + if (['circle', 'line', 'fill'].includes(type)) { + const layerStyle = layerHighlightStyle[type]; + const highlightLayer = Object.assign({}, layer, { + id: this.createHightlightLayerId(id), + type, + paint: Object.assign({}, paint, LAYER_DEFAULT_STYLE[type].paint, layerStyle?.paint), + layout: Object.assign({}, LAYER_DEFAULT_STYLE[type].layout, layerStyle?.layout), + filter + }); + this.map.addLayer(highlightLayer as mapboxglTypes.AnyLayer); + this.highlightOptions.layerIds = this.uniqueLayerIds(this.highlightOptions.layerIds.concat(id)); + } + if (type === 'fill') { + const layerStyle = layerHighlightStyle.strokeLine; + const highlightLayer = Object.assign({}, layer, { + id: this.createHighlightStrokeLayerId(id), + type: 'line', + paint: Object.assign({}, LAYER_DEFAULT_STYLE['strokeLine'].paint, layerStyle?.paint), + layout: Object.assign({}, LAYER_DEFAULT_STYLE['strokeLine'].layout, layerStyle?.layout), + filter + }); + this.map.addLayer(highlightLayer as mapboxglTypes.AnyLayer); + } + } + + updateHighlightDatas(data: UpdateHighlightOptions) { + // @ts-ignore + const matchLayer = this.map.getLayer(data.layerId).serialize(); + const features = data.features.map(item => { + return { + ...item, + layer: matchLayer + }; + }); + this.dataSelectorMode = DataSelectorMode.ALL; + this.handleMapSelections(features); + } + + removeHighlightLayers() { + if (!this.map) { + return; + } + const layersToRemove = this.getHighlightLayerIds(this.highlightOptions.layerIds); + layersToRemove.forEach(layerId => { + if (this.map.getLayer(layerId)) { + this.map.removeLayer(layerId); + } + }); + } + + createPopupFeatureInfo(feature: LayerClickedFeature, targetId: string) { + let displayFieldsList = this.highlightOptions.displayFieldsMap?.[targetId]; + if (!displayFieldsList || !displayFieldsList.length) { + displayFieldsList = (this.highlightOptions.featureFieldsMap?.[targetId] ?? Object.keys(feature.properties)).map( + item => { + return { + field: item, + title: item + }; + } + ); + } + const featureInfo: PopupFeatureInfo = { + coordinates: this.calcFeatureCenterCoordinates(feature), + info: displayFieldsList.reduce((list: PopupFieldsInfo[], item) => { + if (feature.properties[item.field]) { + list.push({ + attribute: item.field, + alias: item.title, + attributeValue: feature.properties[item.field], + slotName: item.slotName + }); + } + return list; + }, []) + }; + return featureInfo; + } + + clear() { + this.removeHighlightLayers(); + this.activeTargetId = null; + this.resultFeatures = []; + this.dataSelectorMode = DataSelectorMode.SINGLE; + } + + destroy() { + this.clear(); + this.unregisterLayerMouseEvents(); + this.unregisterLayerMultiClick(); + this.unregisterMapClick(); + } + + private calcFeatureCenterCoordinates(feature: LayerClickedFeature) { + const geometry = feature.geometry; + if ( + geometry.type === 'MultiPolygon' || + geometry.type === 'Polygon' || + geometry.type === 'LineString' || + geometry.type === 'MultiLineString' + ) { + return getFeatureCenter(feature); + } + return geometry.coordinates; + } + + private createFilterExp({ + feature, + targetId, + fields = this.highlightOptions.featureFieldsMap?.[targetId] + }: CreateFilterExpParams) { + // 高亮过滤(所有字段) + const filterKeys = ['smx', 'smy', 'lon', 'lat', 'longitude', 'latitude', 'x', 'y', 'usestyle', 'featureinfo']; + const isBasicType = (item: any) => { + return typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'; + }; + const filter: any[] = ['all']; + const featureKeys: string[] = fields || feature._vectorTileFeature?._keys || Object.keys(feature.properties); + return featureKeys.reduce((exp, key) => { + if (filterKeys.indexOf(key.toLowerCase()) === -1 && isBasicType(feature.properties[key])) { + exp.push(['==', key, feature.properties[key]]); + } + return exp; + }, filter); + } + + private createLayerHighlightStyle(types: StyleTypes, layerId: string) { + const highlightStyle: HighlightStyle = JSON.parse(JSON.stringify(this.highlightOptions.style)); + types + .filter(type => PAINT_BASIC_ATTRS[type]) + .forEach(type => { + if (!highlightStyle[type]) { + // @ts-ignore + highlightStyle[type] = HIGHLIGHT_DEFAULT_STYLE[type]; + } + const paintBasicAttrs = PAINT_BASIC_ATTRS[type]; + paintBasicAttrs.forEach(paintType => { + if (!highlightStyle[type].paint?.[paintType]) { + const originPaintValue = + type !== 'strokeLine' && this.map.getLayer(layerId) && this.map.getPaintProperty(layerId, paintType); + highlightStyle[type].paint = Object.assign({}, highlightStyle[type].paint, { + [paintType]: originPaintValue || PAINT_DEFAULT_STYLE[paintType] + }); + } + }); + }); + return highlightStyle; + } + + private transHighlightStyle(highlightStyle: HighlightStyle) { + const nextHighlightStyle = JSON.parse(JSON.stringify(highlightStyle)); + // 兼容 strokeLine 错误写法 stokeLine + if ('stokeLine' in highlightStyle && !('strokeLine' in highlightStyle)) { + nextHighlightStyle.strokeLine = highlightStyle.stokeLine; + delete nextHighlightStyle.stokeLine; + } + return nextHighlightStyle; + } + + private getHighlightLayerIds(layerIds: string[]) { + return layerIds.reduce((idList, layerId) => { + const highlightLayerId = this.createHightlightLayerId(layerId); + const highlightStrokeLayerId = this.createHighlightStrokeLayerId(layerId); + idList.push(highlightLayerId, highlightStrokeLayerId); + return idList; + }, []); + } + + private uniqueLayerIds(layerIds: string[]) { + return Array.from(new Set(layerIds)); + } + + private createHightlightLayerId(layerId: string) { + return `${layerId}-${this.highlightOptions.name}-SM-highlighted`; + } + + private createHighlightStrokeLayerId(layerId: string) { + const highlightLayerId = this.createHightlightLayerId(layerId); + return `${highlightLayerId}-StrokeLine`; + } + + private handleMapClick(e: mapboxglTypes.MapLayerMouseEvent) { + const features = this.queryLayerFeatures(e as mapboxglTypes.MapLayerMouseEvent); + if (this.dataSelectorMode !== DataSelectorMode.MULTIPLE) { + this.dataSelectorMode = DataSelectorMode.SINGLE; + } + this.activeTargetId = this.dataSelectorMode === DataSelectorMode.MULTIPLE ? features[0]?.layer?.id : null; + this.handleMapSelections(features); + } + + private handleMapSelections(features: LayerClickedFeature[]) { + this.removeHighlightLayers(); + let popupDatas: PopupFeatureInfo[] = []; + const matchTargetFeature = features[0]; + let activeTargetLayer = matchTargetFeature?.layer; + if (activeTargetLayer) { + switch (this.dataSelectorMode) { + case DataSelectorMode.ALL: + this.resultFeatures = features; + break; + case DataSelectorMode.MULTIPLE: { + const id = matchTargetFeature.id || getValueCaseInsensitive(matchTargetFeature.properties, 'smid'); + if ( + !id || + !this.resultFeatures.map(item => item.id || getValueCaseInsensitive(item.properties, 'smid')).includes(id) + ) { + this.resultFeatures.push(matchTargetFeature); + } + break; + } + default: + this.resultFeatures = [matchTargetFeature]; + break; + } + const params: CreateRelatedDatasParams = { + features: this.resultFeatures, + targetId: activeTargetLayer.id, + isMultiple: this.dataSelectorMode !== DataSelectorMode.SINGLE + }; + const filterExps = this.createFilterExps(params); + popupDatas = this.createPopupDatas(params); + this.addHighlightLayers(activeTargetLayer, filterExps); + } + const emitData: MapSelectionChangedEmit = { + features, + popupInfos: popupDatas.map(item => item.info), + lnglats: popupDatas.map(item => item.coordinates), + highlightLayerIds: this.getHighlightLayerIds(this.highlightOptions.layerIds), + dataSelectorMode: this.dataSelectorMode + }; + this.fire('mapselectionchanged', emitData); + } + + private handleLayerKeydown(e: KeyboardEvent) { + if (e.ctrlKey && this.dataSelectorMode !== DataSelectorMode.MULTIPLE) { + this.handleMapSelections([]); + this.dataSelectorMode = DataSelectorMode.MULTIPLE; + } + } + + private handleLayerKeyup(e: KeyboardEvent) { + if (e.key === 'Control') { + this.dataSelectorMode = DataSelectorMode.SINGLE; + } + } + + private handleMapMouseEnter() { + this.map.getCanvas().style.cursor = 'pointer'; + } + + private handleMapMouseLeave() { + this.map.getCanvas().style.cursor = ''; + } + + private queryLayerFeatures(e: mapboxglTypes.MapLayerMouseEvent) { + const map = e.target; + const bbox = [ + [e.point.x - this.highlightOptions.clickTolerance, e.point.y - this.highlightOptions.clickTolerance], + [e.point.x + this.highlightOptions.clickTolerance, e.point.y + this.highlightOptions.clickTolerance] + ] as unknown as [mapboxglTypes.PointLike, mapboxglTypes.PointLike]; + const features = map.queryRenderedFeatures(bbox, { + layers: this.activeTargetId + ? [this.activeTargetId] + : this.highlightOptions.layerIds.filter(item => !!this.map.getLayer(item)) + }) as unknown as LayerClickedFeature[]; + return features; + } + + private createFilterExps(params: CreateRelatedDatasParams) { + const { features, targetId, isMultiple } = params; + return features.reduce( + (filterExps: any[], feature) => { + const filterExp = this.createFilterExp({ feature, targetId }); + if (isMultiple) { + filterExps.push(filterExp); + } else { + filterExps = filterExp; + } + return filterExps; + }, + ['any'] + ); + } + + private createPopupDatas(params: CreateRelatedDatasParams) { + const { features, targetId, isMultiple } = params; + return features.reduce((popupDatas: PopupFeatureInfo[], feature) => { + const popupInfo = this.createPopupFeatureInfo(feature, targetId); + if (isMultiple) { + popupDatas.push(popupInfo); + } else { + popupDatas = [popupInfo]; + } + return popupDatas; + }, []); + } +} diff --git a/src/mapboxgl/layer-highlight/__tests__/LayerHighlightViewModel.spec.js b/src/mapboxgl/layer-highlight/__tests__/LayerHighlightViewModel.spec.js new file mode 100644 index 000000000..e1a791133 --- /dev/null +++ b/src/mapboxgl/layer-highlight/__tests__/LayerHighlightViewModel.spec.js @@ -0,0 +1,225 @@ +import LayerHighlightViewModel from '../LayerHighlightViewModel'; +import Map from '@mocks/map'; + +describe('LayerHighlightViewModel', () => { + const highlightStyle = { + line: { + paint: { + 'line-width': 3, + 'line-color': '#01ffff', + 'line-opacity': 1 + } + }, + circle: { + paint: { + 'circle-color': '#01ffff', + 'circle-opacity': 0.6, + 'circle-radius': 8, + 'circle-stroke-width': 2, + 'circle-stroke-color': '#01ffff', + 'circle-stroke-opacity': 1 + } + }, + fill: { + paint: { + 'fill-color': '#01ffff', + 'fill-opacity': 0.6, + 'fill-outline-color': '#01ffff' + } + }, + stokeLine: { + paint: { + 'line-width': 3, + 'line-color': '#01ffff', + 'line-opacity': 1 + } + } + }; + + let map; + const uniqueName = 'Test'; + const mockLayerName = 'China'; + let viewModel; + + beforeEach(() => { + map = new Map({ + style: { center: [0, 0], zoom: 1, layers: [], sources: {} } + }); + viewModel = new LayerHighlightViewModel({ name: uniqueName, style: highlightStyle }); + viewModel.setMap({ map }); + }); + + afterEach(() => { + viewModel.destroy(); + }); + + it('toogle show highlight layers', done => { + const pointLayer = { + id: 'pointLayer', + type: 'circle' + }; + viewModel.addHighlightLayers(pointLayer); + const layers = map.getStyle().layers; + expect(layers.length).toBe(1); + expect(layers[0].id).toBe(`${pointLayer.id}-${uniqueName}-SM-highlighted`); + expect(viewModel.highlightOptions.layerIds).toEqual([pointLayer.id]); + viewModel.removeHighlightLayers(); + expect(map.getStyle().layers.length).toBe(0); + expect(viewModel.highlightOptions.layerIds).toEqual([pointLayer.id]); + viewModel.setTargetLayers([]); + expect(viewModel.highlightOptions.layerIds).toEqual([]); + done(); + }); + + it('map click target layer by specified featureFields', done => { + viewModel.once('mapselectionchanged', ({ features }) => { + expect(features.length).toBeGreaterThan(0); + const layers = map.getStyle().layers; + expect(layers.length).toBe(2); + expect(layers[0].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted`); + expect(layers[1].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted-StrokeLine`); + expect(layers[0].filter[0]).toBe('all'); + expect(layers[1].filter[0]).toBe('all'); + expect(viewModel.highlightOptions.layerIds).toEqual([mockLayerName]); + viewModel.removeHighlightLayers(); + expect(map.getStyle().layers.length).toBe(0); + expect(viewModel.highlightOptions.layerIds).toEqual([mockLayerName]); + viewModel.unregisterMapClick(); + done(); + }); + expect(viewModel.featureFields).toBeUndefined(); + const featureFieldsMap = { [mockLayerName]: ['title', 'subtitle', 'imgUrl', 'description', 'index'] }; + viewModel.setFeatureFieldsMap(featureFieldsMap); + expect(viewModel.highlightOptions.featureFieldsMap).toEqual(featureFieldsMap); + viewModel.map.fire('click', { target: map, point: { x: 10, y: 5 } }); + }); + + it('map click target layer by pressing ctrl', done => { + const keyboardEvents = {}; + jest.spyOn(window, 'addEventListener').mockImplementation((type, cb) => { + keyboardEvents[type] = cb; + }); + viewModel.once('mapselectionchanged', ({ features, dataSelectorMode }) => { + expect(features.length).toBe(0); + expect(dataSelectorMode).toBe('SINGLE'); + viewModel.once('mapselectionchanged', ({ features, dataSelectorMode }) => { + expect(dataSelectorMode).toBe('MULTIPLE'); + expect(features.length).toBeGreaterThan(0); + const layers = map.getStyle().layers; + expect(layers.length).toBe(2); + expect(layers[0].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted`); + expect(layers[1].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted-StrokeLine`); + expect(layers[0].filter[0]).toBe('any'); + expect(layers[1].filter[0]).toBe('any'); + done(); + }); + Promise.resolve().then(() => { + viewModel.map.fire('click', { target: map, point: { x: 10, y: 5 } }); + }); + }); + viewModel.setMultiSelection(true); + expect(keyboardEvents.keydown).not.toBeUndefined(); + expect(keyboardEvents.keyup).not.toBeUndefined(); + keyboardEvents.keydown({ ctrlKey: 'Control' }); + }); + + it('map click same feature', done => { + const keyboardEvents = {}; + jest.spyOn(window, 'addEventListener').mockImplementation((type, cb) => { + keyboardEvents[type] = cb; + }); + viewModel.once('mapselectionchanged', ({ features, dataSelectorMode }) => { + expect(features.length).toBe(0); + expect(dataSelectorMode).toBe('SINGLE'); + viewModel.once('mapselectionchanged', ({ features, dataSelectorMode }) => { + expect(dataSelectorMode).toBe('MULTIPLE'); + expect(features.length).toBeGreaterThan(0); + const layers = map.getStyle().layers; + expect(layers.length).toBe(2); + expect(layers[0].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted`); + expect(layers[1].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted-StrokeLine`); + const layer1Filter = layers[0].filter; + const layer2Filter = layers[1].filter; + expect(layer1Filter[0]).toBe('any'); + expect(layer2Filter[0]).toBe('any'); + viewModel.once('mapselectionchanged', ({ features: nextFeatures, dataSelectorMode }) => { + expect(dataSelectorMode).toBe('MULTIPLE'); + expect(nextFeatures.length).toBe(features.length); + const layers = map.getStyle().layers; + expect(layers.length).toBe(2); + expect(layers[0].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted`); + expect(layers[1].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted-StrokeLine`); + const nextLayer1Filter = layers[0].filter; + const nextLayer2Filter = layers[1].filter; + expect(nextLayer1Filter).toEqual(layer1Filter); + expect(nextLayer2Filter).toEqual(layer2Filter); + done(); + }); + Promise.resolve().then(() => { + viewModel.map.fire('click', { target: map, point: { x: 10, y: 5 } }); + }); + }); + Promise.resolve().then(() => { + viewModel.map.fire('click', { target: map, point: { x: 10, y: 5 } }); + }); + }); + viewModel.setMultiSelection(true); + expect(keyboardEvents.keydown).not.toBeUndefined(); + expect(keyboardEvents.keyup).not.toBeUndefined(); + keyboardEvents.keydown({ ctrlKey: 'Control' }); + }); + + it('updateHighlightDatas', done => { + const pointLayer = { + id: mockLayerName, + type: 'circle' + }; + map.addLayer(pointLayer); + const layers = map.getStyle().layers; + expect(layers.length).toBe(1); + viewModel.once('mapselectionchanged', ({ features, dataSelectorMode }) => { + expect(dataSelectorMode).toBe('ALL'); + expect(features.length).toBeGreaterThan(0); + const layers = map.getStyle().layers; + expect(layers.length).toBe(2); + expect(layers[0].id).toBe(mockLayerName); + expect(layers[1].id).toBe(`${mockLayerName}-${uniqueName}-SM-highlighted`); + expect(layers[1].filter[0]).toBe('any'); + done(); + }); + viewModel.updateHighlightDatas({ + features: [{ properties: { name: '11' }, geometry: { type: 'Point', coordinates: [0, 0] } }], + layerId: mockLayerName + }); + expect(viewModel.dataSelectorMode).toBe('ALL'); + }); + + it('map click empty', done => { + viewModel.once('mapselectionchanged', ({ features }) => { + expect(features.length).toBe(0); + viewModel.unregisterMapClick(); + done(); + }); + viewModel.map.fire('click', { target: map, point: { x: 0, y: 5 } }); + }); + + it('map canvas style', done => { + const canvaStyle = {}; + const events = {}; + jest.spyOn(map, 'getCanvas').mockImplementation(() => ({ + style: canvaStyle + })); + jest.spyOn(map, 'on').mockImplementation((type, layerId, cb) => { + events[type] = cb ? cb : layerId; + }); + const targetIds = ['Test1']; + viewModel.setTargetLayers(targetIds); + events.mousemove(); + expect(viewModel.highlightOptions.layerIds).toEqual(targetIds); + expect(canvaStyle.cursor).toBe('pointer'); + events.mouseleave(); + expect(canvaStyle.cursor).toBe(''); + done(); + }); +}); + diff --git a/src/mapboxgl/layer-highlight/__tests__/LayersHighlight.spec.js b/src/mapboxgl/layer-highlight/__tests__/LayersHighlight.spec.js new file mode 100644 index 000000000..e99db1dae --- /dev/null +++ b/src/mapboxgl/layer-highlight/__tests__/LayersHighlight.spec.js @@ -0,0 +1,201 @@ +import { mount } from '@vue/test-utils'; +import SmLayerHighlight from '../LayerHighlight.vue'; +import SmMapPopup from '../../map-popup/MapPopup.vue'; +import SmWebMap from '../../web-map/WebMap'; + +const highlightStyle = { + line: { + paint: { + 'line-width': 3, + 'line-color': '#01ffff', + 'line-opacity': 1 + } + }, + circle: { + paint: { + 'circle-color': '#01ffff', + 'circle-opacity': 0.6, + 'circle-radius': 8, + 'circle-stroke-width': 2, + 'circle-stroke-color': '#01ffff', + 'circle-stroke-opacity': 1 + } + }, + fill: { + paint: { + 'fill-color': '#01ffff', + 'fill-opacity': 0.6, + 'fill-outline-color': '#01ffff' + } + }, + stokeLine: { + paint: { + 'line-width': 3, + 'line-color': '#01ffff', + 'line-opacity': 1 + } + } +}; + +describe('LayerHighlight.vue', () => { + let wrapper, mapWrapper; + beforeAll(() => { + mapWrapper = mount(SmWebMap, { + propsData: { + serverUrl: 'https://fakeiportal.supermap.io/iportal', + mapId: '123' + } + }); + }); + beforeEach(() => { + wrapper = null; + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + mapWrapper.destroy(); + } + }); + + it('render default correctly', done => { + wrapper = mount(SmLayerHighlight, { + propsData: { + mapTarget: 'map', + uniqueName: 'Test', + layers: ['layer1'], + highlightStyle + } + }); + wrapper.vm.$nextTick(); + expect(wrapper.find(SmMapPopup).exists()).toBe(true); + expect(wrapper.vm.tablePopupProps.columns.length).toBe(2); + expect(wrapper.vm.tablePopupProps.showHeader).toBe(false); + done(); + }); + + it('set layers', done => { + wrapper = mount(SmLayerHighlight, { + propsData: { + mapTarget: 'map', + uniqueName: 'Test', + highlightStyle: {} + } + }); + const setSpy = jest.spyOn(wrapper.vm.viewModel, 'setTargetLayers'); + const clearSpy = jest.spyOn(wrapper.vm.viewModel, 'clear'); + wrapper.setProps({ layers: ['layer1'] }); + wrapper.vm.$nextTick(); + expect(setSpy).toBeCalled(); + expect(clearSpy).toBeCalled(); + done(); + }); + + it('set highlightStyle', done => { + wrapper = mount(SmLayerHighlight, { + propsData: { + mapTarget: 'map', + uniqueName: 'Test', + highlightStyle: {} + } + }); + const setSpy = jest.spyOn(wrapper.vm.viewModel, 'setHighlightStyle'); + wrapper.setProps({ highlightStyle }); + wrapper.vm.$nextTick(); + expect(setSpy).toBeCalled(); + done(); + }); + + it('set multiSelection', done => { + wrapper = mount(SmLayerHighlight, { + propsData: { + mapTarget: 'map', + uniqueName: 'Test', + highlightStyle: {}, + multiSelection: false + } + }); + const setSpy = jest.spyOn(wrapper.vm.viewModel, 'setMultiSelection'); + wrapper.setProps({ multiSelection: true }); + wrapper.vm.$nextTick(); + expect(setSpy).toBeCalled(); + done(); + }); + + it('set featureFieldsMap', done => { + wrapper = mount(SmLayerHighlight, { + propsData: { + mapTarget: 'map', + uniqueName: 'Test', + highlightStyle: {} + } + }); + const setSpy = jest.spyOn(wrapper.vm.viewModel, 'setFeatureFieldsMap'); + wrapper.setProps({ featureFieldsMap: {} }); + wrapper.vm.$nextTick(); + expect(setSpy).toBeCalled(); + done(); + }); + + it('set featureFieldsMap', done => { + wrapper = mount(SmLayerHighlight, { + propsData: { + mapTarget: 'map', + uniqueName: 'Test', + highlightStyle: {} + } + }); + const setSpy = jest.spyOn(wrapper.vm.viewModel, 'setDisplayFieldsMap'); + wrapper.setProps({ displayFieldsMap: {} }); + wrapper.vm.$nextTick(); + expect(setSpy).toBeCalled(); + done(); + }); + + it('set clickTolerance', done => { + wrapper = mount(SmLayerHighlight, { + propsData: { + mapTarget: 'map', + uniqueName: 'Test', + highlightStyle: {} + } + }); + const setSpy = jest.spyOn(wrapper.vm.viewModel, 'clickTolerance'); + wrapper.setProps({ clickTolerance: 8 }); + wrapper.vm.$nextTick(); + expect(setSpy).toBeCalled(); + done(); + }); + + it('events', done => { + wrapper = mount(SmLayerHighlight, { + propsData: { + mapTarget: 'map', + uniqueName: 'Test', + highlightStyle: {} + }, + stubs: ['SmMapPopup'] + }); + const params = { + features: [ + { properties: { name: '11' }, geometry: { type: 'Point', coordinates: [0, 0] } }, + { properties: { name: '22' }, geometry: { type: 'Point', coordinates: [1, 2] } } + ], + popupInfos: [ + [{ attribute: 'key1', attributeValue: 'value1', alias: 'alias1', slotName: 1 }], + [{ attribute: 'key2', attributeValue: 'value2', alias: 'alias2', slotName: 2 }] + ], + lnglats: [ + [0, 0], + [1, 2] + ] + }; + wrapper.vm.viewModel.fire('mapselectionchanged', params); + expect(wrapper.emitted().mapselectionchanged).toBeTruthy(); + expect(wrapper.vm.allPopupDatas).toEqual(params.popupInfos); + expect(wrapper.vm.lnglats).toEqual(params.lnglats); + expect(wrapper.vm.currentIndex).toBe(1); + done(); + }); +}); + diff --git a/src/mapboxgl/layer-highlight/index.js b/src/mapboxgl/layer-highlight/index.js new file mode 100644 index 000000000..581e79e86 --- /dev/null +++ b/src/mapboxgl/layer-highlight/index.js @@ -0,0 +1,9 @@ +import LayerHighlight from './LayerHighlight'; +import init from 'vue-iclient/src/init'; + +LayerHighlight.install = function(Vue, opts) { + init(Vue, opts); + Vue.component(LayerHighlight.options ? LayerHighlight.options.name : LayerHighlight.name, LayerHighlight); +}; + +export default LayerHighlight; diff --git a/src/mapboxgl/layer-highlight/style/index.js b/src/mapboxgl/layer-highlight/style/index.js new file mode 100644 index 000000000..8f6454fef --- /dev/null +++ b/src/mapboxgl/layer-highlight/style/index.js @@ -0,0 +1,4 @@ +import 'vue-iclient/src/common/_assets/iconfont/icon-sm-components.css'; +import 'vue-iclient/src/common/_utils/style/common/common.scss'; + +import './layer-highlight.scss'; diff --git a/src/mapboxgl/layer-highlight/style/layer-highlight.scss b/src/mapboxgl/layer-highlight/style/layer-highlight.scss new file mode 100644 index 000000000..c3db1200b --- /dev/null +++ b/src/mapboxgl/layer-highlight/style/layer-highlight.scss @@ -0,0 +1,14 @@ +@import '../../../common/_utils/style/mixins/mixins.scss'; +@import '../../../common/_utils/style/theme/theme.scss'; +@import '../../map-popup/style/map-popup.scss'; + +@include b(layer-highlight) { + border-radius: 4px; + .sm-component-table-popup__table { + &.sm-component-table-wrapper { + .sm-component-table-content { + overflow-x: hidden; + } + } + } +} diff --git a/src/mapboxgl/map-popup/MapPopup.vue b/src/mapboxgl/map-popup/MapPopup.vue index 2d235fa9d..df92cf8a2 100644 --- a/src/mapboxgl/map-popup/MapPopup.vue +++ b/src/mapboxgl/map-popup/MapPopup.vue @@ -71,6 +71,8 @@ class SmMapPopup extends Mixins(MapGetter, Theme) { }) columns: Array; + @Prop({ default: true }) showHeader: Boolean; + @Watch('currentCoordinate') currentCoordinatesChanged() { this.addPopup(); @@ -86,12 +88,25 @@ class SmMapPopup extends Mixins(MapGetter, Theme) { setPopupArrowStyle(this.tablePopupBgData); } + @Watch('data') + dataChanged() { + if (!this.data.length) { + this.removePopup(); + } + } + get currentCoordinate() { return this.lnglats[this.currentIndex]; } get tablePopupProps() { - return { data: this.data[this.currentIndex], columns: this.columns }; + return { + data: this.data[this.currentIndex], + columns: this.columns, + showHeader: this.showHeader, + background: 'transparent', + color: 'inherit' + }; } get headerTitle() { diff --git a/src/mapboxgl/query/Query.vue b/src/mapboxgl/query/Query.vue index 764a0e525..809aedf0b 100644 --- a/src/mapboxgl/query/Query.vue +++ b/src/mapboxgl/query/Query.vue @@ -1,44 +1,99 @@ - +