Skip to content
目录

自己写个dom的聚合插件

2023-12-19_100131.png

有些业务里对聚合点的要求需要强烈的自定义效果(动画,CSS特效,聚合点里包含表格等特殊需求时),这时用webgl或者canvas去绘制聚合点就比较不 方便了,这时我们可以基于 UIMarker去自定义个聚合插件

WARNING

由于dom渲染的性能问题,所以改方案的性能相对于canvas/webgl等是比较差的,能用canvas/webgl绘制方法解决的最好 使用canvas/webgl方案

设计

抽象个图层MarkerClusterLayer

  • 承载点的集合,提供动态设置数据
  • 内置聚合库,当地图视角动态变化时,获取当前视野内的聚合点
  • 利用UIMarker作为渲染方案,并管理UIMarker的集合
  • 开放自定义UIMarker内容给用户,方便用户自定义UIMarker的内容

图层聚合

这里利用 supercluster 来完成点集的聚合

聚合点的展示

因为聚合点需要强烈的自定义效果,dom渲染是最好的方案了,故这里我们使用UIMarker作为图形渲染方案

代码

因为抽象的是个图层,所以在代码层面我们最好保持和maptalks里Layer一致的使用习惯和体验,主要包括:

  • addTo方法
  • remove方法
  • 其他等

WARNING

注意这里只是提供个基础的代码,并没有做到和maptalks Layer一样的功能,业务可以根据自己的需要动态添加你的功能代码, 这里侧重点是提供相关思路和解决方法,比如图层的:

  • show

  • hide

  • zoomFilter等

这些功能我故意的没有做,交给用户自己去加,以此来锻炼用户的动手能力,而不是只会CV

js
const { Eventable, Class } = maptalks;

const OPTIONS = {
    radius: 250,
    maxZoom: 18,
};

function now() {
    return new Date().getTime();
}

class MarkerClusterLayer extends Eventable(Class) {
    constructor(options) {
        super(options);
        this.data = null;
        this.map = null;
        if (!options.createIcon) {
            console.error("not find createIcon params for create uimarker");
            return this;
        }
        const index = new Supercluster(Object.assign({}, OPTIONS, options));
        this.index = index;
        this.clusterCache = {};
        this.time = now();
    }

    getMap() {
        return this.map;
    }

    getMarkers() {
        return Object.values(this.clusterCache);
    }

    getIndex() {
        return this.index;
    }

    addTo(map) {
        if (this.getMap()) {
            console.error("it has added to map");
            return this;
        }
        this.map = map;
        this.map.on("viewchange", this._viewchange, this);
        this._cluster();
        return this;
    }

    remove() {
        this.map.on("viewchange", this._viewchange, this);
        this.map = null;
        return this;
    }

    _viewchange() {
        this._cluster();
    }

    _cluster() {
        const map = this.getMap();
        if (!map) {
            return this;
        }
        if (!this.data || this.data.length === 0 || !this.options.createIcon) {
            return this;
        }
        const bound = map.getExtent();
        const { xmin, ymin, xmax, ymax } = bound;
        const zoom = Math.round(map.getZoom());
        const result = this.index.getClusters([xmin, ymin, xmax, ymax], zoom);
        const tempCache = {};
        result.forEach((feature) => {
            const { id } = feature;
            tempCache[id] = feature;
        });
        for (const key in this.clusterCache) {
            if (!tempCache[key]) {
                const marker = this.clusterCache[key];
                marker.remove();
                delete marker.feature;
                delete this.clusterCache[key];
            }
        }

        // const time = 'time';
        // console.time(time);
        for (const key in tempCache) {
            if (this.clusterCache[key]) {
                continue;
            }
            const feature = tempCache[key];
            const marker = this.options.createIcon(feature);
            if (!marker) {
                continue;
            }
            marker.addTo(map);
            marker.feature = tempCache[key];
            this.clusterCache[key] = marker;
        }
        // console.timeEnd(time);
        return this;
    }

    _checkData(geojson) {
        let features;
        if (geojson.type === "FeatureCollection") {
            features = geojson.features || [];
        } else if (Array.isArray(geojson)) {
            features = geojson;
        }
        if (!features) {
            console.error("geojson data is error", geojson);
            return this;
        }
        features.forEach((feature) => {
            feature.id = feature.id || `f-${maptalks.Util.GUID()}`;
        });
        this.index.load(features);
        this.data = geojson;
        return this;
    }

    setData(geojson) {
        this._checkData(geojson);
        this._cluster();
        return this;
    }

    clear() {
        this.data = [];
        this.getMarkers().forEach((marker) => {
            marker.remove();
        });
        this.clusterCache = {};
        this._cluster();
        return this;
    }
}

MarkerClusterLayer.mergeOptions(OPTIONS);

使用

因为设计时参考的是Layer,所以其使用起来和其他的图层是一样的习惯

js
const markerClusterLayer = new MarkerClusterLayer({ createIcon });
markerClusterLayer.addTo(map);

完整的业务代码

js
var map = new maptalks.Map("map", {
    center: [121.52413252, 31.14154476],
    zoom: 11,
    pitch: 0,
    zoomControl: true,
    baseLayer: new maptalks.TileLayer("base", {
        urlTemplate:
            "https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
        subdomains: ["a", "b", "c", "d"],
        maxAvailableZoom: 18,
    }),
});

const markerClick = (e) => {
    console.log(e.target);
};

const createIcon = (feature) => {
    const cluster = feature.properties.cluster;
    const coordinate = feature.geometry.coordinates;
    let marker;
    if (!cluster) {
        marker = new maptalks.ui.UIMarker(coordinate, {
            content: '<img src="./../assets/image/people.gif"/>',
        });
    } else {
        const count = feature.properties.point_count;
        const id = feature.id;
        const index = markerClusterLayer.getIndex();
        //https://github.com/mapbox/supercluster
        const features = index.getLeaves(id, 10);
        // console.log(features);
        const size = count < 100 ? "small" : count < 1000 ? "medium" : "large";
        marker = new maptalks.ui.UIMarker(coordinate, {
            content: `<div class="marker-cluster marker-cluster-${size}">${count}</div>`,
        });
    }
    marker.on("click", markerClick);
    return marker;
};

const markerClusterLayer = new MarkerClusterLayer({ createIcon });
markerClusterLayer.addTo(map);

fetch("./../assets/data/pois.geojson")
    .then((res) => res.json())
    .then((geojson) => {
        markerClusterLayer.setData(geojson);
    });

完整代码

This document is generated by mdpress