当前位置: 首页 > 工具软件 > RBush > 使用案例 >

openlayer 基于Rbush的海量数据展示

上官兴昌
2023-12-01
import RBush from "rbush";
import { readEncWkt_4326 } from "@/utils/geoUtil";
import { allLayers, totalLoadLayers } from "@/utils/layerUtil";
import SphericalMercator from "@mapbox/sphericalmercator";
import request from "@/utils/request";
import { transformExtent } from "ol/proj";
import { openDB, deleteDB } from 'idb';
import { isEqArrays } from '@/utils/index';

let globalIndexDB;
export const MapTileMixin = {
    computed: {
    },
    data() {
        return {
            rtree: undefined,
            rectangles: [],
            tiles: {},
            moveTiles: [],
            queryTiles: [],
            lastQueryTiles:[],

            loadTiles: [],
            maptiles: {},
            mapDataDbName: "interiorDataDbName-" + this.internalPackageId
        }
    },
    created() {
        this.rtree = new RBush();
    },
    methods: {
        deleteIndexDB() {
            deleteDB(this.mapDataDbName, {
                blocked() {
                    try {
                        if(globalIndexDB){
                            globalIndexDB.close();
                        }
                    } catch (error) {
                        console.info("db删除完毕",e);
                    }
                }
            });
        },
        initDB(cb) {
            this.deleteIndexDB();
            const dbPromise = openDB(
                this.mapDataDbName,
                undefined,
                {
                    upgrade(db) {
                        //创建表,每个矢量图层一张表
                        allLayers.forEach(layerCode => {
                            db.createObjectStore(layerCode, { keyPath: 'id' });
                        });
                    }
                }
            );
            dbPromise.then((db) => {
                globalIndexDB = db;
                
            }).finally(()=>{
                cb();
            });
        },
        clearTilesAll() {
            this.maptiles = {};
            this.mapObj.getLayers().getArray().forEach(layer => {
                if (layer.getSource && layer.getSource() && layer.getSource().clear) {
                    layer.getSource().clear();
                }
            })
        },
        clearTilesByLayerCode(layerCode) {
            this.clearCacheTile(layerCode);
            this.mapObj.getLayers().getArray().forEach(layer => {
                if (layer.getProperties().code == layerCode) {
                    if (layer.getSource && layer.getSource() && layer.getSource().getFeatures) {
                        console.error("当前缓存清除图层:" + layerCode + "  矢量元素数量:" + layer.getSource().getFeatures().length);
                    }
                    layer.getSource().clear();
                }
            })
        },
        clearCacheTile(layerCode){
            let _this = this;
            Object.keys(this.maptiles).forEach((key) => {
                if (key.split("-")[0] == layerCode) {
                    delete _this.maptiles[key];
                }
            })
        },
        clearTileByCoord(coord) {
            let _this = this;
            Object.keys(this.maptiles).forEach((key) => {
                let bbox = _this.maptiles[key];
                if (coord[0] >= bbox[0] && coord[0] <= bbox[2]
                    && coord[1] >= bbox[1] && coord[1] <= bbox[3]) {
                    delete _this.maptiles[key];
                }
            })
        },
        clearTileAndDbByExtent(layerCodes) {
            let _this = this;
            layerCodes.forEach(async code => {
                for (let key in _this.maptiles) {
                    let tileId = key.split('-')[1] + "-" + key.split('-')[2] + "-" + key.split('-')[3];
                    if (_this.queryTiles.indexOf(tileId) > -1) {
                        delete _this.maptiles[key];
                        globalIndexDB.delete(code, tileId);
                    }
                }
            })
        },
        clearTreeAndDbByCode(layerObjArr) {
            new Promise(r => {
                this.rtree.all().forEach(async item => {
                    for (let i = 0, len = layerObjArr.length; i < len; i++) {
                        if (layerObjArr[i].layerCode == item.storeName) {
                            this.rtree.remove(item);
                            const storeData = await globalIndexDB.getAll(item.storeName);
                            for (let d of storeData) {
                                await globalIndexDB.delete(item.storeName, d.id);
                            }
                            break;
                        }
                    }
                })
            })
        },
        updateTreeAndDbData() {
            new Promise(r => {
                this.rtree.all().forEach(async item => {
                    for (let i = 0, len = layerObjArr.length; i < len; i++) {
                        if (layerObjArr[i].layerCode == item.storeName) {
                            this.rtree.remove(item);
                            const storeData = await globalIndexDB.getAll(item.storeName);
                            for (let d of storeData) {
                                await globalIndexDB.delete(item.storeName, d.id);
                            }
                            break;
                        }
                    }
                })
            })
        },
        tilesByLyerCodeOutMax(layerCodes, Max) {
            let _this = this;
            layerCodes.forEach(code => {
                let cacheLayerData = _this.getTilesCacheCountByLayerCode(code);
                if (cacheLayerData.tileCacheNum > Max) {
                    console.error("当前缓存清除图层:" + code + "  瓦片数量:" + cacheLayerData.tileCacheNum);
                    _this.clearTilesByLayerCode(code);
                }
            })
        },
        getTilesCacheCountByLayerCode(layerCode) {
            let cacheLayerData = {
                tileCacheNum: 0,
                cacheDataNum: 0
            }
            Object.keys(this.maptiles).forEach(key => {
                if (key.split("-")[0] == layerCode) {
                    cacheLayerData.tileCacheNum = cacheLayerData.tileCacheNum + 1;
                }
            })
            this.mapObj.getLayers().getArray().forEach(layer => {
                if (layer.getProperties().code == layerCode) {
                    cacheLayerData.cacheDataNum = layer.getSource().getFeatures().length;
                }
            })
            return cacheLayerData;
        },
        isQueryLayerData(layerInfo) {
            let isQuery = true;
            if (layerInfo.layerCode) {
                let layers = this.mapObj.getLayers().getArray();
                let count = 0;
                for (let k = 0, l = layerInfo.layerCode.length; k < l; k++) {
                    for (let i = 0, len = layers.length; i < len; i++) {
                        if (layerInfo.layerCode[k] == layers[i].getProperties().code && !layers[i].getVisible()) {
                            count = count + 1;
                        }
                    }
                }
                if (count == layerInfo.layerCode.length) {
                    isQuery = false;
                }
            }
            return isQuery;
        },
        getLoadTiles() {
            // 设置缩放小于15的时候不加载
            const zoom = Math.floor(this.mapObj.getView().getZoom());
            if (zoom < 18) {
                this.queryTiles = []
                this.lastQueryTiles = []
                return
            }
            this.lastQueryTiles = this.queryTiles
            this.queryTiles = this.getTilesByZoom(17);
        },
        // 根据zoom和bounds获取屏幕范围内的网格
        getTilesByZoom(queryZoom) {
            let mapBounds = transformExtent(this.mapObj.getView().calculateExtent(), "EPSG:3857", "EPSG:4326");
            const southWest = [mapBounds[0], mapBounds[1]]
            const northEast = [mapBounds[2], mapBounds[3]]
            // -1为了使网格范围大,请求次数减少
            const swTiles = this.queryTilesByLatlng(southWest, queryZoom);
            const neTiles = this.queryTilesByLatlng(northEast, queryZoom);
            const tileNos = this.containTiles(swTiles, neTiles, queryZoom);

            return tileNos
        },
        /*
        * 根据坐标和缩放级别获取所在瓦片
        * 【param】 latlng 坐标
        * 【param】 zoom 缩放级别
        *  return
        */
        queryTilesByLatlng(latlng, zoom) {
            const p = this.project(latlng)
            const scale1 = this.scale(zoom)
            const _a = 0.5 / (Math.PI * 6378137)
            const _b = 0.5
            const _c = -0.5 / (Math.PI * 6378137)
            const _d = 0.5
            const x = scale1 * (_a * p[0] + _b)
            const y = scale1 * (_c * p[1] + _d)
            const tileX = Math.floor(x / 256)
            const tileY = Math.floor(y / 256)
            return [tileX, tileY]
        },
        project(latlng) {
            const [x, y] = latlng
            const R = 6378137
            const MAX_LATITUDE = 85.0511287798
            const d = Math.PI / 180
            const max = MAX_LATITUDE
            const lat = Math.max(Math.min(max, y), -max)
            const sin = Math.sin(lat * d)
            return [R * x * d, (R * Math.log((1 + sin) / (1 - sin))) / 2]
        },
        scale(zoom) {
            return 256 * Math.pow(2, zoom)
        },
        /*
        * 计算屏幕范围所覆盖所有瓦片
        * 【param】 lb 左下瓦片号
        * 【param】 rt 右上瓦片号
        *  return
        */
        containTiles(lb, rt, zoom) {
            const [l, b] = lb
            const [r, t] = rt
            const tiles = []
            let dx = l
            while (dx <= r) {
                let dy = b
                while (dy >= t) {
                    tiles.push(dx + '-' + dy + '-' + zoom)
                    dy--
                }
                dx++
            }
            return tiles
        },
        /**
         * tile 转 BBOX
         * @param {*} x
         * @param {*} y
         * @param {*} z
         */
        zxyToBbox(x, y, z) {
            let merc = new SphericalMercator({
                size: 256
            });
            let bbox = merc.bbox(x, y, z, false, "WGS84")
            return {
                // bbox: transformExtent(bbox, "EPSG:3857", "EPSG:4326"),
                bbox: bbox,
                id: x + "-" + y + "-" + z
            }
        },
        totalLoadData(url, requestParams, layerInfo, storeName) {
            // 图层主键字段,用于存indexDB库时作主键和存rtree的关键字段
            const { layerPrimaryKey, layerGeometry, layerBbox } = layerInfo;
            return new Promise(async resolve => {
                let dataList;
                // 若indexDB有当前图层的数据,则直接获取并装载rtree
                // 每次挪动地图触发TileLoadLayer时走这个逻辑
                const totalData = await globalIndexDB.getAll(storeName);
                if (totalData && totalData.length) {
                    // 取出所有的表内真实数据
                    dataList = totalData.map(v => v.data);

                    // 计算每条数据的extend并装载rtree
                    let insertions = [];
                    dataList.forEach((entity) => {
                        // 计算每条数据的extend并装载rtree
                        const extend = this.entityExtent(entity, layerGeometry);
                        extend && insertions.push({
                            ...extend,
                            storeName,
                            id: entity[layerPrimaryKey]
                        })
                    });

                    if (insertions.length > 0) {
                        this.rtree.load(insertions);
                    }

                    resolve(dataList);
                    return
                }

                // 全量请求internalPackageId所有数据,所以范围传全球[-180, -90, 180, 90]
                // 后续可考虑后端增加参数ignoreBbox来处理达到同样的效果
                const tile = {
                    bbox: layerBbox || [-180, -90, 180, 90]
                }
                this.loadData(url, tile, requestParams, layerInfo.isPost)
                    .then(async ({ data }) => {
                        dataList = data;
                        let insertions = [];
                        for (let i = 0; i < dataList.length; i++) {
                            let entity = dataList[i];

                            // 将加载的数据逐条存入indexDB
                            // TODO 可以考虑在这里创建索引
                            await globalIndexDB.put(storeName, {
                                id: entity[layerPrimaryKey],
                                data: entity
                            });

                            // 计算每条数据的extend并装载rtree
                            const extend = this.entityExtent(entity, layerGeometry);
                            extend && insertions.push({
                                ...extend,
                                storeName,
                                id: entity[layerPrimaryKey]
                            })
                        }

                        if (insertions.length > 0) {
                            this.rtree.load(insertions);
                        }
                        resolve(dataList)
                    })
            })
        },
        totalInMapViewData(layerInfo, storeName) {
            const mapObj = this.mapObj;
            const { layerPrimaryKey } = layerInfo;
            const extent = transformExtent(mapObj.getView().calculateExtent(mapObj.getSize()), "EPSG:3857", "EPSG:4326")
            return new Promise(async resolve => {
                const result = [];
                // 根据地图的extent从rtree计算数据的主键列表
                const viewData = this.intersects(extent).filter(item => item.storeName === storeName);
                for (let item of viewData) {

                    // 根据数据的主键列表,从indexDB读出所有数据
                    const { data } = await globalIndexDB.get(storeName, item[layerPrimaryKey]) || {};
                    data && result.push(data)
                }
                resolve({
                    data: result
                });
            })
        },
        handleTreeData(dataList, layerInfo) {
            const { layerCode, layerFilter, beforeProcess, postProcess } = layerInfo
            return new Promise(async resolve => {
                // 返回对象
                const result = {};

                // 前置处理
                beforeProcess ? beforeProcess(dataList) : null;

                // 遍历图层处理:
                for (const code of layerCode) {

                    // 将数据由图层过滤后,组装到返回对象和存储对象内
                    result[`${code}List`] = [];

                    dataList.forEach((data) => {
                        const filter = (layerFilter || {})[code];
                        const layerData = filter ? filter(data.data || []) : data.data;
                        result[`${code}List`] = result[`${code}List`].concat(...layerData)
                    });

                }

                // 后置处理
                postProcess ? postProcess(dataList) : null;

                resolve(result);
            })
        },
        totalLoadLayer(url, requestParams, layerInfo, cb) {
            // 库的名字,注意不能使用LayerCode.join(','),因为初始创建都是用单个code创建的
            // 所以这里可以固定取第一个code即可
            const storeName = layerInfo.layerCode[0];

            let promise;
            // 先尝试rtree内是否有数据,如果有,则不需要请求及装载
            const hasData = this.rtree.all().findIndex(x => x.storeName === storeName);

            if (hasData < 0) {
                // 如果rtree内没有数据,则加载全量一次数据
                promise = this.totalLoadData(url, requestParams, layerInfo, storeName);
            } else {
                // 如果rtree没有数据,则跳过
                promise = new Promise(resolve => setTimeout(() => { resolve() }));
            }

            // 确保rtree内有数据后,获取当前视图范围内数据
            promise
                .then(data => {
                    // 当前地图视图范围,过滤出需要渲染的数据
                    return this.totalInMapViewData(layerInfo, storeName)
                })
                .then((data) => {
                    // 对数据做分图层处理
                    return this.handleTreeData([data], layerInfo)
                })
                .then((data) => {
                    this.mapObj.getLayers().getArray().forEach(layer => {
                        if (layerInfo.layerCode.indexOf(layer.getProperties().code) > -1) {
                            layer.getSource().clear();
                        }
                    })
                    cb(data, layerInfo)
                })
                .catch(ignore => { })

        },
        /**
         * 矢量数据请求
         * @param {} extent
         * @param {*} url
         * @param {*} requestParams
         * @param {*} layerInfo
         * @param {*} cb
         */
        TileLoadLayer(url, requestParams, layerInfo, cb) {
            // if(layerInfo.layerCode[0] != 'road'){return}
            let _this = this;
            if (!this.isQueryLayerData(layerInfo)) {
                return;
            }

            // 配置全量请求图层,且有内包ID时,一次请求整包数据
            const isTotalLayer = totalLoadLayers.findIndex(v => isEqArrays(v, layerInfo.layerCode));
            if (requestParams.internalPackageId && isTotalLayer > -1) {
                this.totalLoadLayer(url, requestParams, layerInfo, cb);
                return;
            }

            let newTileNos = [];
            this.queryTiles.forEach(newTile => {
                let xyz = newTile.split("-");
                let tile = _this.zxyToBbox(Number(xyz[0]), Number(xyz[1]), Number(xyz[2]));
                let count = 0;
                for (let i = 0, len = layerInfo.layerCode.length; i < len; i++) {
                    let code = layerInfo.layerCode[i];
                    if (!_this.maptiles[code + "-" + newTile]) {
                        _this.maptiles[code + "-" + newTile] = tile.bbox;
                        count = count + 1;
                    }
                }
                if (count == layerInfo.layerCode.length) {
                    newTileNos.push(tile);
                }
            })

          
            if (newTileNos.length == 0) {
                // console.error("开始读取数据库缓存数据");
                // console.log(this.existChangeTile())
                if(this.existChangeTile()){
                    this.getCacheDataList(this.queryTiles,layerInfo.layerCode).then(result=>{
                        cb(result, layerInfo)
                    })
                }
                return;
            }

            Promise.all(
                newTileNos.map(tile =>
                    this.loadData(url, tile, requestParams, layerInfo.isPost)
                )
            ).then(resArr => {
                let dataList = [];
                for (let idx = 0; idx < resArr.length; idx++) {
                    dataList.push(
                        {
                            id: newTileNos[idx].id,
                            data: resArr[idx].data
                        }
                    );
                }
                return this.handleData(dataList, layerInfo, false)
            }).then(() => {
                this.getCacheDataList(this.queryTiles,layerInfo.layerCode).then(result=>{
                    cb(result, layerInfo)
                })
            }).catch(function (err) {
                newTileNos.map(tile => {
                    layerInfo.layerCode.forEach(code => {
                        if (_this.maptiles[code + tile.id]) {
                            delete _this.maptiles[code + tile.id];
                        }
                    })
                }
                )
            });
        },
        existChangeTile(){
            return this.queryTiles.find(q=>!this.lastQueryTiles.find(l=>l ==q))
        },
        async getCacheDataList(tiles,layerCodes){
            let result = {}
            for (const code of layerCodes) {

                // ①将数据由图层过滤后,组装到返回对象和存储对象内
                result[`${code}List`] = [];

                // ①将组装的每个瓦片数据存储到indexDB,并获取所有应该查询瓦片数据
                if (globalIndexDB.objectStoreNames.contains(code)) {
                    
                    for (const tileId of tiles) {
                        try {
                            const row = await globalIndexDB.get(code, tileId)
                            if (row && row.data) {
                                result[`${code}List`] = result[`${code}List`].concat(row.data)
                            }
                        } catch (error) {
                            console.error(code + "---------------------");
                            console.error(tileId);
                        }
                    }

                }

            }
            return result;
        },
        loadData(url, tile, requestParams, isPost) {
            requestParams = requestParams || {};
            let params = {};
            params.swLng = tile.bbox[0].toFixed(6);
            params.neLng = tile.bbox[2].toFixed(6);
            params.swLat = tile.bbox[1].toFixed(6);
            params.neLat = tile.bbox[3].toFixed(6);
            params = Object.assign({}, params, requestParams);
            if (isPost) {
                return request({
                    url,
                    data: params,
                    method: "post"
                });
            }
            return request({
                url,
                params: params
            });
        },
        handleData(dataList, layerInfo, isQueryTilesAll) {
            const { layerCode, layerFilter, beforeProcess, postProcess } = layerInfo
            return new Promise(async resolve => {
                // 返回对象
                const result = {};
                // 存储对象
                const tables = {};

                // 前置处理
                beforeProcess ? beforeProcess(dataList) : null;

                // 遍历图层处理:
                for (const code of layerCode) {

                    // ①将数据由图层过滤后,组装到返回对象和存储对象内
                    result[`${code}List`] = [];
                    tables[`${code}`] = [];

                    dataList.forEach((data) => {
                        const filter = (layerFilter || {})[code];
                        const layerData = filter ? filter(data.data || []) : data.data;
                        tables[`${code}`].push({
                            id: data.id,
                            data: layerData
                        });
                    });

                    // ①将组装的每个瓦片数据存储到indexDB,并获取所有应该查询瓦片数据
                    if (globalIndexDB.objectStoreNames.contains(code)) {
                        for (const tile of tables[code]) {
                            await globalIndexDB.put(code, tile)
                        }
                        if (isQueryTilesAll) {
                            for (const tileId of this.queryTiles) {
                                try {
                                    const row = await globalIndexDB.get(code, tileId)
                                    if (row) {
                                        result[`${code}List`] = result[`${code}List`].concat(row.data)
                                    }
                                } catch (error) {
                                    console.error(code + "---------------------");
                                    console.error(tileId);
                                }
                            }
                        } else {
                            for (const tile of tables[code]) {
                                result[`${code}List`] = result[`${code}List`].concat(tile.data)
                            }
                        }

                    }

                }
                // 后置处理
                postProcess ? postProcess(dataList) : null;

                resolve(result);
            })
        },
        entityExtent(entity, layerGeometry) {
            const { type, field } = layerGeometry;
            let extent = [];
            switch (type) {
                // 如果是点数据,直接取点坐标,且extent就是本身这个点的范围
                case 'Point':
                    extent = [
                        Number(entity[field[0]]),
                        Number(entity[field[1]]),
                        Number(entity[field[0]]),
                        Number(entity[field[1]]),
                    ];
                    break;

                // 如果是线或者面数据,直接取对应的wkt字段,并计算extent
                case 'Polyline':
                case 'Polygon':
                    let geom = entity[field];
                    if (geom) {
                        extent = readEncWkt_4326(geom).getGeometry().getExtent();
                    }
                    break;
                default:
                    break;
            }
            // 如果计算失败,返回空,针对null则不装载rtree即可
            if (extent.length < 4) {
                return null
            }
            return {
                minX: extent[0],
                minY: extent[1],
                maxX: extent[2],
                maxY: extent[3]
            }
        },
        intersects(extent) {
            const bbox = {
                minX: extent[0],
                minY: extent[1],
                maxX: extent[2],
                maxY: extent[3]
            };
            return this.rtree.search(bbox);
        },
    },
    watch: {
    }

}

 类似资料: