From 7f81003e21fe02ef803f08a6254a88405a5f88ab Mon Sep 17 00:00:00 2001 From: xiongjj Date: Fri, 11 Oct 2024 14:03:53 +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=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=9C=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E9=AB=98=E4=BA=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mapboxgl/_utils/HightlighLayer.ts | 316 ++++++++++++++++++ .../_utils/__tests__/HighlightLayer.spec.js | 150 +++++++++ src/mapboxgl/query/Query.vue | 35 +- src/mapboxgl/query/QueryViewModel.js | 36 +- src/mapboxgl/query/__tests__/Query.spec.js | 74 +++- .../query/__tests__/QueryViewModel.spec.js | 116 +++++++ .../web-map/control/identify/Identify.vue | 2 +- .../control/identify/IdentifyViewModel.js | 114 +------ test/unit/mocks/map.js | 14 +- 9 files changed, 736 insertions(+), 121 deletions(-) create mode 100644 src/mapboxgl/_utils/HightlighLayer.ts create mode 100644 src/mapboxgl/_utils/__tests__/HighlightLayer.spec.js create mode 100644 src/mapboxgl/query/__tests__/QueryViewModel.spec.js diff --git a/src/mapboxgl/_utils/HightlighLayer.ts b/src/mapboxgl/_utils/HightlighLayer.ts new file mode 100644 index 00000000..b20ec161 --- /dev/null +++ b/src/mapboxgl/_utils/HightlighLayer.ts @@ -0,0 +1,316 @@ +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; + + 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[]) { + 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); + 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); + } + } + + 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); + } + }); + } + + _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.filterFields)); + } + 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; + } + + _createFilterExp(feature: LayerClickedFeature, fields?: string[]) { + // 高亮过滤(所有字段) + 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); + } +} diff --git a/src/mapboxgl/_utils/__tests__/HighlightLayer.spec.js b/src/mapboxgl/_utils/__tests__/HighlightLayer.spec.js new file mode 100644 index 00000000..03699f61 --- /dev/null +++ b/src/mapboxgl/_utils/__tests__/HighlightLayer.spec.js @@ -0,0 +1,150 @@ +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/query/Query.vue b/src/mapboxgl/query/Query.vue index c04fd866..1b2c358f 100644 --- a/src/mapboxgl/query/Query.vue +++ b/src/mapboxgl/query/Query.vue @@ -151,6 +151,36 @@ export default { }; } }, + highlightStyle: { + type: Object, + default() { + return { + line: new LineStyle({ + 'line-width': 3, + 'line-color': '#01ffff', + 'line-opacity': 1 + }), + circle: new CircleStyle({ + 'circle-color': '#01ffff', + 'circle-opacity': 0.6, + 'circle-radius': 8, + 'circle-stroke-width': 2, + 'circle-stroke-color': '#01ffff', + 'circle-stroke-opacity': 1 + }), + fill: new FillStyle({ + 'fill-color': '#01ffff', + 'fill-opacity': 0.6, + 'fill-outline-color': '#01ffff' + }), + strokeLine: new LineStyle({ + 'line-width': 3, + 'line-color': '#01ffff', + 'line-opacity': 1 + }) + }; + } + }, iportalData: { type: Array }, @@ -205,6 +235,9 @@ export default { }, layerStyle() { this.viewModel && (this.viewModel.layerStyle = this.$props.layerStyle); + }, + highlightStyle(next) { + this.viewModel && this.viewModel.setHighlightStyle(JSON.parse(JSON.stringify(next))); } }, mounted() { @@ -335,7 +368,7 @@ export default { this.popup && this.popup.remove() && (this.popup = null); this.jobInfo = null; this.activeResultIndex = null; - this.viewModel && this.viewModel.removed(); + this.viewModel && this.viewModel.clear(); }, getInfoOfSmid(properties) { return `SmID:${getValueCaseInsensitive(properties, 'smid')}`; diff --git a/src/mapboxgl/query/QueryViewModel.js b/src/mapboxgl/query/QueryViewModel.js index 713a9407..d6e224e5 100644 --- a/src/mapboxgl/query/QueryViewModel.js +++ b/src/mapboxgl/query/QueryViewModel.js @@ -6,6 +6,8 @@ import bbox from '@turf/bbox'; import envelope from '@turf/envelope'; import transformScale from '@turf/transform-scale'; import getFeatures from 'vue-iclient/src/common/_utils/get-features'; +import HighlightLayer from 'vue-iclient/src/mapboxgl/_utils/HightlighLayer'; + /** * @class QueryViewModel * @classdesc 查询组件功能类。 @@ -23,31 +25,42 @@ import getFeatures from 'vue-iclient/src/common/_utils/get-features'; * @fires QueryViewModel#queryfailed * @fires QueryViewModel#getfeatureinfosucceeded */ -export default class QueryViewModel extends mapboxgl.Evented { +export default class QueryViewModel extends HighlightLayer { constructor(options) { - super(); + super({ name: 'query', style: options.highlightStyle }); this.options = options || {}; this.maxFeatures = this.options.maxFeatures || 200; this.layerStyle = options.layerStyle || {}; + this._handleMapSelectionChanged = this._handleMapSelectionChanged.bind(this); } setMap(mapInfo) { const { map } = mapInfo; this.map = map; + this.registerMapClick(); + this.on('mapselectionchanged', this._handleMapSelectionChanged); } clearResultLayer() { if (this.map) { this.strokeLayerID && this.map.getLayer(this.strokeLayerID) && this.map.removeLayer(this.strokeLayerID); this.layerID && this.map.getLayer(this.layerID) && this.map.removeLayer(this.layerID); + this.removeHighlightLayers(); + this.unregisterLayerMouseEvents(); } } - removed() { + clear() { this.bounds = null; this.clearResultLayer(); } + removed() { + this.clear(); + this.unregisterMapClick(); + this.off('mapselectionchanged', this._handleMapSelectionChanged); + } + /** * @function QueryViewModel.prototype.query * @desc 开始查询。 @@ -59,7 +72,7 @@ export default class QueryViewModel extends mapboxgl.Evented { return; } this.queryParameter = queryParameter; - this.removed(); + this.clear(); this.queryBounds = queryBounds; if (queryBounds === 'currentMapBounds') { this.bounds = this.map.getBounds(); @@ -78,6 +91,7 @@ export default class QueryViewModel extends mapboxgl.Evented { return; } this.queryResult = { name: queryParameter.name, result: res.features }; + this.setFilterFields(res.fields); this._addResultLayer(this.queryResult); this.fire('querysucceeded', { result: this.queryResult }); } catch (error) { @@ -87,7 +101,7 @@ export default class QueryViewModel extends mapboxgl.Evented { } _addResultLayer() { - this.layerID = this.queryParameter.name + new Date().getTime(); + this.layerID = `${this.queryParameter.name}-SM-query-result`; let type = this.queryResult.result[0].geometry.type; let source = { type: 'geojson', @@ -105,7 +119,6 @@ export default class QueryViewModel extends mapboxgl.Evented { ], { maxZoom: 17 } ); - this.getPopupFeature(); } /** @@ -154,8 +167,8 @@ export default class QueryViewModel extends mapboxgl.Evented { * @function QueryViewModel.prototype.getPopupFeature * @desc 获得地图点击位置的要素信息。调用此方法后,需要监听 'getfeatureinfosucceeded' 事件获得要素。 */ - getPopupFeature() { - this.map.on('click', this.layerID, e => { + getPopupFeature(e) { + if (e.features.length > 0) { let feature = e.features[0]; let featureInfo = this._getFeatrueInfo(feature); /** @@ -164,7 +177,7 @@ export default class QueryViewModel extends mapboxgl.Evented { * @property {Object} e - 事件对象。 */ this.fire('getfeatureinfosucceeded', { featureInfo }); - }); + } } /** @@ -242,5 +255,10 @@ export default class QueryViewModel extends mapboxgl.Evented { paint: lineStyle }); } + this.registerLayerMouseEvents([layerID, this.strokeLayerID].filter(item => !!item)); + } + + _handleMapSelectionChanged(e) { + this.getPopupFeature(e); } } diff --git a/src/mapboxgl/query/__tests__/Query.spec.js b/src/mapboxgl/query/__tests__/Query.spec.js index b6093dbf..8f2686d9 100644 --- a/src/mapboxgl/query/__tests__/Query.spec.js +++ b/src/mapboxgl/query/__tests__/Query.spec.js @@ -348,6 +348,78 @@ describe('query', () => { expect(queryModeDom.text()).toBe('query.keyQueryCondition'); wrapper.find(SmButton).find('.sm-component-query__a-button').trigger('click'); expect(spyquery).toBeCalled(); - }) + }); + + it('query clear result', async (done) => { + wrapper = mount(SmQuery, { + localVue, + propsData: { + mapTarget: 'map', + restData: [ + new RestDataParameter({ + url: 'https://fakeiserver.supermap.io/iserver/services/data-world/rest/data', + attributeFilter: 'SmID>0', + maxFeatures: 30, + dataName: ['World:Countries'], + queryMode: 'KEYWORD' + }) + ] + }, + }); + await mapSubComponentLoaded(wrapper); + expect(wrapper.vm.mapTarget).toBe('map'); + const spyquery = jest.spyOn(wrapper.vm, 'query'); + wrapper.vm.viewModel.on('querysucceeded', res => { + expect(res.result.result[0].properties['名称']).toBe('四川省'); + expect(wrapper.vm.activeTab).toBe('result'); + let resultHeader = wrapper.find('.sm-component-query__result-header i'); + expect(resultHeader.exists()).toBeTruthy(); + const clearSpy = jest.spyOn(wrapper.vm.viewModel, 'clear'); + resultHeader.trigger('click'); + expect(wrapper.find('.sm-component-query__result-header i').exists()).toBeFalsy(); + expect(clearSpy).toBeCalled(); + done(); + }); + const queryModeDom = wrapper.find('.sm-component-query__job-info-body .sm-component-query__item-holder div') + expect(queryModeDom.exists()).toBeTruthy(); + expect(queryModeDom.text()).toBe('query.keyQueryCondition'); + wrapper.find(SmButton).find('.sm-component-query__a-button').trigger('click'); + expect(spyquery).toBeCalled(); + }); + + it('update highlight style', async (done) => { + wrapper = mount(SmQuery, { + localVue, + propsData: { + mapTarget: 'map', + restData: [ + new RestDataParameter({ + url: 'https://fakeiserver.supermap.io/iserver/services/data-world/rest/data', + attributeFilter: 'SmID>0', + maxFeatures: 30, + dataName: ['World:Countries'], + queryMode: 'KEYWORD' + }) + ] + }, + }); + await mapSubComponentLoaded(wrapper); + expect(wrapper.vm.highlightStyle).not.toBeUndefined(); + const nextHighlightStyle = JSON.parse(JSON.stringify(wrapper.vm.highlightStyle)); + nextHighlightStyle.circle = { + paint: { + 'circle-color': 'red', + 'circle-opacity': 0.6, + 'circle-radius': 18, + 'circle-stroke-width': 2, + 'circle-stroke-color': '#01ffff', + 'circle-stroke-opacity': 1 + } + }; + const setSpy = jest.spyOn(wrapper.vm.viewModel, 'setHighlightStyle'); + wrapper.setProps({ highlightStyle: nextHighlightStyle }); + expect(setSpy).toBeCalled(); + done(); + }); }); diff --git a/src/mapboxgl/query/__tests__/QueryViewModel.spec.js b/src/mapboxgl/query/__tests__/QueryViewModel.spec.js new file mode 100644 index 00000000..ea1b4a1a --- /dev/null +++ b/src/mapboxgl/query/__tests__/QueryViewModel.spec.js @@ -0,0 +1,116 @@ +import QueryViewModel from '../QueryViewModel'; +import { FetchRequest } from 'vue-iclient/static/libs/iclient-common/iclient-common'; +import RestDataParameter from '@types_common/RestDataParameter'; +import { + REST_DATA_FIELDS_RESULT, + dataset_data, + prj_data, + iportal_content, + fakeDataServiceResult, + fakeMapServiceResult, + datas +} from '@mocks/services'; +import Map from '@mocks/map'; + +describe('QueryViewMode', () => { + 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 + } + } + }; + + const options = { + restData: [ + new RestDataParameter({ + url: 'https://fakeiserver.supermap.io/iserver/services/data-world/rest/data', + attributeFilter: 'SmID>0', + maxFeatures: 30, + dataName: ['World:Countries'], + queryMode: 'KEYWORD' + }) + ], + highlightStyle + }; + let mapEvents = {}, + map; + beforeEach(() => { + const mockImplementationCb = url => { + if (url.includes('/123')) { + return Promise.resolve(new Response(JSON.stringify(datas))); + } + if (url.includes('/content')) { + return Promise.resolve(new Response(JSON.stringify(iportal_content))); + } + if (url.includes('/fields')) { + return Promise.resolve(new Response(JSON.stringify(REST_DATA_FIELDS_RESULT))); + } + if (url.includes('/prjCoordSys')) { + return Promise.resolve(new Response(JSON.stringify(prj_data))); + } + if (url.includes('/queryResults')) { + return Promise.resolve(new Response(JSON.stringify(fakeMapServiceResult))); + } + if (url.includes('/featureResults')) { + return Promise.resolve(new Response(JSON.stringify(fakeDataServiceResult))); + } + return Promise.resolve(new Response(JSON.stringify(dataset_data))); + }; + jest.spyOn(FetchRequest, 'get').mockImplementation(mockImplementationCb); + jest.spyOn(FetchRequest, 'post').mockImplementation(mockImplementationCb); + mapEvents = {}; + map = { + on: (type, cb) => { + mapEvents[type] = cb; + }, + off: type => { + delete mapEvents[type]; + } + }; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('listen map click', done => { + const viewModel = new QueryViewModel(options); + const map = new Map({ + style: { center: [0, 0], zoom: 1, layers: [], sources: {} } + }); + viewModel.setMap({ map }); + viewModel.on('getfeatureinfosucceeded', ({ featureInfo }) => { + expect(featureInfo.info.length).toBeGreaterThan(0); + done(); + }); + map.fire('click', { target: map, point: { x: 10, y: 5 } }); + }); +}); + diff --git a/src/mapboxgl/web-map/control/identify/Identify.vue b/src/mapboxgl/web-map/control/identify/Identify.vue index f5d7c935..66c22b04 100644 --- a/src/mapboxgl/web-map/control/identify/Identify.vue +++ b/src/mapboxgl/web-map/control/identify/Identify.vue @@ -88,7 +88,7 @@ export default { 'fill-opacity': 0.6, 'fill-outline-color': '#409eff' }), - stokeLine: new LineStyle({ + strokeLine: new LineStyle({ 'line-width': 3, 'line-color': '#409eff', 'line-opacity': 1 diff --git a/src/mapboxgl/web-map/control/identify/IdentifyViewModel.js b/src/mapboxgl/web-map/control/identify/IdentifyViewModel.js index 0d121391..26d459af 100644 --- a/src/mapboxgl/web-map/control/identify/IdentifyViewModel.js +++ b/src/mapboxgl/web-map/control/identify/IdentifyViewModel.js @@ -1,4 +1,4 @@ -import mapboxgl from 'vue-iclient/static/libs/mapboxgl/mapbox-gl-enhance'; +import HighlightLayer from 'vue-iclient/src/mapboxgl/_utils/HightlighLayer'; /** * @class IdentifyViewModel @@ -11,18 +11,10 @@ import mapboxgl from 'vue-iclient/static/libs/mapboxgl/mapbox-gl-enhance'; * @param {Object} [options.layerStyle.stokeLine] - 面图层样式配置。 * @extends mapboxgl.Evented */ -const HIGHLIGHT_COLOR = '#01ffff'; -const defaultPaintTypes = { - circle: ['circle-radius', 'circle-stroke-width'], - line: ['line-width'], - fill: ['line-width'] -}; -export default class IdentifyViewModel extends mapboxgl.Evented { +export default class IdentifyViewModel extends HighlightLayer { constructor(map, options) { - super(); + super({ name: 'identify', layerIds: options.layers, style: options.layerStyle }); this.map = map; - this.layers = options.layers || []; - this.layerStyle = options.layerStyle || {}; } /** @@ -31,111 +23,19 @@ export default class IdentifyViewModel extends mapboxgl.Evented { * @param {Object} layer - layer。 */ addOverlayToMap(layer, filter) { - let mbglStyle = { - circle: { - 'circle-color': HIGHLIGHT_COLOR, - 'circle-opacity': 0.6, - 'circle-stroke-color': HIGHLIGHT_COLOR, - 'circle-stroke-opacity': 1 - }, - line: { - 'line-color': HIGHLIGHT_COLOR, - 'line-opacity': 1 - }, - fill: { - 'fill-color': HIGHLIGHT_COLOR, - 'fill-opacity': 0.6, - 'fill-outline-color': HIGHLIGHT_COLOR - }, - symbol: { - layout: { - 'icon-size': 5 - } - } - }; - let { type, id, paint } = layer; - // 如果是面的strokline,处理成面 - if (id.includes('-strokeLine') && type === 'line') { - type = 'fill'; - paint = {}; - } - let layerStyle = this._setDefaultPaintWidth(this.map, type, id, defaultPaintTypes[type], this.layerStyle); - if (type === 'circle' || type === 'line' || type === 'fill') { - const _layerStyle = layerStyle[type]; - let highlightLayer = Object.assign({}, layer, { - id: id + '-identify-SM-highlighted', - type, - paint: (_layerStyle && _layerStyle.paint) || Object.assign({}, paint, mbglStyle[type]), - layout: (_layerStyle && _layerStyle.layout) || { visibility: 'visible' }, - filter - }); - this.map.addLayer(highlightLayer); - } - if (type === 'fill') { - let strokeLayerID = id + '-identify-SM-StrokeLine'; - let stokeLineStyle = layerStyle.strokeLine || layerStyle.stokeLine || {}; - let lineStyle = (stokeLineStyle && stokeLineStyle.paint) || { - 'line-width': 3, - 'line-color': HIGHLIGHT_COLOR, - 'line-opacity': 1 - }; - let highlightLayer = Object.assign({}, layer, { - id: strokeLayerID, - type: 'line', - paint: lineStyle, - layout: { visibility: 'visible' }, - filter - }); - this.map.addLayer(highlightLayer); - } - // if(type === 'symbol') { - // let layout = Object.assign({}, layer.layout, {'icon-size': layer.layout['icon-size'] + 2}) - // let highlightLayer = Object.assign({}, layer, { - // id: layerID + '-highlighted', - // layout, - // filter - // }); - // this.map.addLayer(highlightLayer); - // } + this.addHighlightLayers(layer, filter); } /** * @function IdentifyViewModel.prototype.removed * @desc 清除高亮图层。 */ - removed(layers = this.layers) { + removed(layers) { // 移除高亮图层 this.removeOverlayer(layers); } - removeOverlayer(layers = this.layers) { - layers && - layers.forEach(layerId => { - this.map && - this.map.getLayer(layerId + '-identify-SM-highlighted') && - this.map.removeLayer(layerId + '-identify-SM-highlighted'); - this.map && - this.map.getLayer(layerId + '-identify-SM-StrokeLine') && - this.map.removeLayer(layerId + '-identify-SM-StrokeLine'); - }); - } - - _setDefaultPaintWidth(map, type, layerId, paintTypes, layerStyle) { - if (!paintTypes) { - return; - } - paintTypes.forEach(paintType => { - let mapPaintProperty; - if (type !== 'fill') { - mapPaintProperty = map.getLayer(layerId) && map.getPaintProperty(layerId, paintType); - } else { - type = 'stokeLine'; - } - layerStyle[type].paint[paintType] = layerStyle[type].paint[paintType] || mapPaintProperty; - if (layerStyle[type].paint[paintType] === void 0 || layerStyle[type].paint[paintType] === '') { - layerStyle[type].paint[paintType] = paintType === 'circle-stroke-width' || type === 'stokeLine' ? 2 : 8; - } - }); - return layerStyle; + removeOverlayer(layers) { + this.removeHighlightLayers(layers); } } diff --git a/test/unit/mocks/map.js b/test/unit/mocks/map.js index eee46e0e..c9e43d76 100644 --- a/test/unit/mocks/map.js +++ b/test/unit/mocks/map.js @@ -350,7 +350,15 @@ var Map = function (options) { return style; }; - this.removeLayer = function (layerId) {}; + this.removeLayer = function (layerId) { + if(this.addedLayers[layerId]) { + delete this.addedLayers[layerId]; + return; + } + if(this.overlayLayersManager[layerId]){ + delete this.overlayLayersManager[layerId]; + } + }; this.moveLayer = function (layerId) {}; this.getFilter = function (layerId) {}; this.setFilter = function (layerId, filter) {}; @@ -447,7 +455,8 @@ var Map = function (options) { const feature = [ { layer: { - id: 'China' + id: 'China', + type: 'fill' }, geometry: { type: 'Point', coordinates: [0, 1] }, type: 'Point', @@ -457,6 +466,7 @@ var Map = function (options) { subtitle: '树正沟景点-老虎海', imgUrl: './laohuhai.png', description: '老虎海海拔2298米', + flag: true, index: 1 }, _vectorTileFeature: {