OpenLayers集群样式异常:Vue3缩放后红色标记消失求助
OpenLayers集群缩放后红色标记消失的解决方案
问题背景
我使用Vue3选项式API开发,希望将包含"Просрочен"(过期)状态点位的OpenLayers集群标记为红色,但缩放地图后红色标记会消失。检查发现缩放时集群的ol_uid属性会变化,尝试重新渲染地图也无效果。相关代码如下:
import View from 'ol/View' import Map from 'ol/Map' import TileLayer from 'ol/layer/Tile' import OSM from 'ol/source/OSM' import VectorLayer from 'ol/layer/Vector' import Point from 'ol/geom/Point'; import LineString from 'ol/geom/LineString'; import VectorSource from 'ol/source/Vector'; import Feature from 'ol/Feature' import { Circle as CircleStyle, Fill, Stroke, Style, Text, } from 'ol/style'; import Overlay from 'ol/Overlay'; import Cluster from "ol/source/Cluster" import { useGeographic } from 'ol/proj'; import 'ol/ol.css' import './style.css' import axios from 'axios' export default { name: 'MapContainer', components: {}, props: {}, data() { return { place: [43.984506, 56.305298], data: [], featuresPoints: [], featuresLines: [], } }, methods: { async getData() { axios("https://someRestApiLink.com").then(res => { this.data = res.data; this.createFeachers(); this.renderMap(); }); }, setCircleStyle(feature) { const size = feature.get('features').length; let style = styleCache[size]; if (!style) { style = new Style({ image: new CircleStyle({ radius: 10, stroke: new Stroke({ color: '#fff', }), fill: new Fill({ color: '#3399CC', }), }), text: new Text({ text: size.toString(), fill: new Fill({ color: '#fff', }), }), }); styleCache[size] = style; } return style; }, createFeachers() { for (let item of this.data) { let coords = JSON.parse(item.coords); if (coords.length === 1) { let feature = new Feature(new Point(coords[0].reverse())); feature.mydata = item; this.featuresPoints.push(feature); } else { let rightCoords = coords.map(el => el.reverse()); let isValidFeacture = true; for (let i = 0; i < rightCoords.length - 1; i++) { if (Math.abs(rightCoords[i][0] - rightCoords[i + 1][0]) > .01) { isValidFeacture = false; break } } if (!isValidFeacture) continue; let feature = new Feature({ geometry: new LineString(rightCoords) }); feature.setStyle(new Style({ stroke: new Stroke({ color: '#0000ff', width: 3 }) })) feature.mydata = item this.featuresLines.push(feature); } } }, createMap() { return new Map({ target: this.$refs['map-root'], view: new View({ zoom: 12, center: this.place }), layers: [ new TileLayer({ source: new OSM() }), this.createLineLayer(), this.createPointLayer(), ], }); }, createPointLayer() { const styleCache = {}; let cluster = new Cluster({ distance: 15, minDistance: 6, source: new VectorSource({ features: this.featuresPoints, }) }); const mainCluster = new VectorLayer({ source: cluster, style: function (feature) { function calculateFired(cluster, length) { let pointList = cluster.values_.features; let countFired = 0; for (let point of pointList) { if (point.mydata.status === "Просрочен") { countFired++; } } return countFired === length ? "full" : countFired > 0 ? "several" : "none"; } const size = feature.get('features').length; let style = styleCache[size]; if (!style) { let hasFired = calculateFired(feature, size); style = new Style({ image: new CircleStyle({ radius: 10, stroke: new Stroke({ color: hasFired === "none" ? '#fff' : "#f00", }), fill: new Fill({ color: hasFired === "full" ? "#f00" : '#3399CC', }), }), text: new Text({ text: size.toString(), fill: new Fill({ color: '#fff', }), }), }); styleCache[size] = style; } return style; }, }); mainCluster.mydata = this.featuresPoints.map(el => el.mydata); return mainCluster }, createLineLayer() { return new VectorLayer({ source: new VectorSource({ features: this.featuresLines, }), }) }, renderMap() { useGeographic(); const createPopUp = this.createPopUp; var container = document.getElementById("popup"); var content = document.getElementById("popup-content"); const map = this.createMap(); const overlay = new Overlay({ element: container, autoPan: true }); map.on('click', function (e) { let pixel = map.getEventPixel(e.originalEvent); if (document.getElementsByClassName('popup-content').length != 0) { document.getElementsByClassName('popup-content')[0].style.display = 'none' } map.forEachFeatureAtPixel(pixel, function (feature) { let data = feature.mydata ?? feature.values_.features.map(el => el.mydata); let coodinate = e.coordinate; content.innerHTML = createPopUp(data); overlay.setPosition(coodinate); map.addOverlay(overlay); }); }); }, createPopUp(data) { let content = ""; if (data.length) { let sortedData = data.sort((a, b)=> { if (a.time.split(".").reverse().join("-") > b.time.split(".").reverse().join("-")) return 1; if (a.time.split(".").reverse().join("-") < b.time.split(".").reverse().join("-")) return -1; return 0; }); for (let el of sortedData) { content += ` <div class="popup-content__valueBlock"> <div class="popup-content__valueBlock__organization"> ${el.organization}: </div> <div class="popup-content__valueBlock__aimOfWorks"> Тип: ${el.aim_of_works} </div> <div class="popup-content__valueBlock__stripSurface"> Работы ведутся над: ${el.strip_surface} </div> <div class="popup-content__valueBlock__status"> ${el.status} - ${el.finish_date} </div> </div> `; } } else { content = ` <div class="popup-content__valueBlock"> <div class="popup-content__valueBlock__organization"> ${data.organization}: </div> <div class="popup-content__valueBlock__aimOfWorks"> Тип: ${data.aim_of_works} </div> <div class="popup-content__valueBlock__stripSurface"> Работы ведутся над: ${data.strip_surface} </div> <div class="popup-content__valueBlock__status"> ${data.status} - ${data.time} </div> </div> `; } return ` <div class="popup-content"> <span class="close" onclick="closePopup()">x</span> <span class="count">Количество выбранных ордеров: ${data.length ?? 1}</span> ${content} </div> `; }, }, mounted() { this.getData(); }, }
问题原因
- 样式缓存逻辑错误:原代码仅通过集群的
size作为styleCache的key,同一大小但包含不同状态点位的集群会复用错误样式。例如一个size=3的全过期集群(红色),和另一个size=3的无过期集群(蓝色),缩放后后者会复用前者的红色样式,导致显示异常。 - 访问私有属性:直接使用
feature.values_.features获取集群内的点位,这是OpenLayers的私有内部属性,可能在版本更新或内部逻辑变化时失效,且不符合官方API规范。
解决方案
修改createPointLayer中的样式逻辑,调整缓存key为集群size + 过期状态标识,确保不同状态的同大小集群不会复用错误样式,同时替换私有属性访问为官方API:
createPointLayer() { const styleCache = {}; let cluster = new Cluster({ distance: 15, minDistance: 6, source: new VectorSource({ features: this.featuresPoints, }) }); const mainCluster = new VectorLayer({ source: cluster, style: function (feature) { function calculateFired(clusterFeature) { // 用官方API获取集群内要素,替代私有属性values_.features const pointList = clusterFeature.get('features'); const total = pointList.length; let countFired = 0; for (let point of pointList) { if (point.mydata.status === "Просрочен") { countFired++; } } return countFired === total ? "full" : countFired > 0 ? "several" : "none"; } const size = feature.get('features').length; const hasFired = calculateFired(feature); // 缓存key结合size和状态,确保不同状态的同大小集群有独立样式 const cacheKey = `${size}-${hasFired}`; let style = styleCache[cacheKey]; if (!style) { style = new Style({ image: new CircleStyle({ radius: 10, stroke: new Stroke({ color: hasFired === "none" ? '#fff' : "#f00", }), fill: new Fill({ color: hasFired === "full" ? "#f00" : '#3399CC', }), }), text: new Text({ text: size.toString(), fill: new Fill({ color: '#fff', }), }), }); styleCache[cacheKey] = style; } return style; }, }); mainCluster.mydata = this.featuresPoints.map(el => el.mydata); return mainCluster }
说明
- 缓存key改为
${size}-${hasFired},即使集群大小相同,只要状态(全过期/部分过期/无过期)不同,就会生成独立的样式,避免复用错误样式。 - 用
feature.get('features')替代feature.values_.features,保证代码的稳定性和兼容性。 - 缩放地图时,集群会重新计算组合,新集群会根据自身size和状态匹配正确的缓存样式,红色标记不会再消失。
内容的提问来源于stack exchange,提问作者Aleks_Andr_19
相关产品推荐
相关产品推荐

