You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
    },
}

问题原因

  1. 样式缓存逻辑错误:原代码仅通过集群的size作为styleCache的key,同一大小但包含不同状态点位的集群会复用错误样式。例如一个size=3的全过期集群(红色),和另一个size=3的无过期集群(蓝色),缩放后后者会复用前者的红色样式,导致显示异常。
  2. 访问私有属性:直接使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 10:50:16