diff --git a/package.json b/package.json index 50cd30de2b..17c4eab9ad 100644 --- a/package.json +++ b/package.json @@ -197,5 +197,7 @@ "tnpm": { "mode": "yarn" }, - "dependencies": {} + "dependencies": { + "@antv/geo-coord": "^1.0.8" + } } diff --git a/packages/core/src/services/layer/ILayerService.ts b/packages/core/src/services/layer/ILayerService.ts index ce6ebe5b02..96d1f052fc 100644 --- a/packages/core/src/services/layer/ILayerService.ts +++ b/packages/core/src/services/layer/ILayerService.ts @@ -101,6 +101,8 @@ export interface ILayer { layerModelNeedUpdate: boolean; styleNeedUpdate: boolean; layerModel: ILayerModel; + layerChildren: ILayer[]; // 在图层中添加子图层 + sceneContainer: Container | undefined; dataState: IDataState; // 数据流状态 pickedFeatureID: number | null; hooks: { @@ -137,7 +139,7 @@ export interface ILayer { needPick(type: string): boolean; getLayerConfig(): Partial; getContainer(): Container; - setContainer(container: Container): void; + setContainer(container: Container, sceneContainer: Container): void; setCurrentPickId(id: number | null): void; getCurrentPickId(): number | null; setCurrentSelectedId(id: number | null): void; @@ -377,7 +379,7 @@ export interface ILayerService { getLayers(): ILayer[]; getLayer(id: string): ILayer | undefined; getLayerByName(name: string): ILayer | undefined; - remove(layer: ILayer): void; + remove(layer: ILayer, parentLayer?: ILayer): void; removeAllLayers(): void; updateRenderOrder(): void; renderLayers(type?: string): void; diff --git a/packages/core/src/services/layer/LayerService.ts b/packages/core/src/services/layer/LayerService.ts index 4eb3b5bac6..9335a261c1 100644 --- a/packages/core/src/services/layer/LayerService.ts +++ b/packages/core/src/services/layer/LayerService.ts @@ -64,11 +64,20 @@ export default class LayerService implements ILayerService { return this.layers.find((layer) => layer.name === name); } - public remove(layer: ILayer): void { - const layerIndex = this.layers.indexOf(layer); - if (layerIndex > -1) { - this.layers.splice(layerIndex, 1); + public remove(layer: ILayer, parentLayer?: ILayer): void { + // Tip: layer.layerChildren 当 layer 存在子图层的情况 + if (parentLayer) { + const layerIndex = parentLayer.layerChildren.indexOf(layer); + if (layerIndex > -1) { + parentLayer.layerChildren.splice(layerIndex, 1); + } + } else { + const layerIndex = this.layers.indexOf(layer); + if (layerIndex > -1) { + this.layers.splice(layerIndex, 1); + } } + layer.emit('remove', null); layer.destroy(); this.renderLayers(); @@ -91,17 +100,29 @@ export default class LayerService implements ILayerService { this.alreadyInRendering = true; this.clear(); this.updateRenderOrder(); + this.layers .filter((layer) => layer.inited) .filter((layer) => layer.isVisible()) .forEach((layer) => { - // trigger hooks - layer.hooks.beforeRenderData.call(); - layer.hooks.beforeRender.call(); - layer.render(); - layer.hooks.afterRender.call(); + // Tip: 渲染 layer 的子图层 默认 layerChildren 为空数组 表示没有子图层 目前只有 ImageTileLayer 有子图层 + renderLayerEvent(layer.layerChildren); + renderLayerEvent([layer]); }); this.alreadyInRendering = false; + + function renderLayerEvent(layers: ILayer[]) { + layers + .filter((layer) => layer.inited) + .filter((layer) => layer.isVisible()) + .forEach((layer) => { + // trigger hooks + layer.hooks.beforeRenderData.call(); + layer.hooks.beforeRender.call(); + layer.render(); + layer.hooks.afterRender.call(); + }); + } } public updateRenderOrder() { @@ -111,7 +132,14 @@ export default class LayerService implements ILayerService { } public destroy() { - this.layers.forEach((layer) => layer.destroy()); + this.layers.forEach((layer) => { + // Tip: layer.layerChildren 当 layer 存在子图层的情况 + if (layer.layerChildren) { + layer.layerChildren.forEach((child) => child.destroy()); + layer.layerChildren = []; + } + layer.destroy(); + }); this.layers = []; this.renderLayers(); } diff --git a/packages/core/src/services/map/IMapService.ts b/packages/core/src/services/map/IMapService.ts index 4e5dc4a38c..32cab61ae3 100644 --- a/packages/core/src/services/map/IMapService.ts +++ b/packages/core/src/services/map/IMapService.ts @@ -187,7 +187,7 @@ export interface IEarthService { ): void; } -export const MapServiceEvent = ['mapload']; +export const MapServiceEvent = ['mapload', 'mapchange']; /** * 地图初始化配置项 diff --git a/packages/layers/src/core/BaseLayer.ts b/packages/layers/src/core/BaseLayer.ts index e190a60c5d..49d8bf3a75 100644 --- a/packages/layers/src/core/BaseLayer.ts +++ b/packages/layers/src/core/BaseLayer.ts @@ -113,6 +113,11 @@ export default class BaseLayer extends EventEmitter public layerModel: ILayerModel; + // TODO: 记录 sceneContainer 供创建子图层的时候使用 如 imageTileLayer + public sceneContainer: Container | undefined; + // TODO: 用于保存子图层对象 + public layerChildren: ILayer[] = []; + @lazyInject(TYPES.IGlobalConfigService) protected readonly configService: IGlobalConfigService; @@ -222,8 +227,9 @@ export default class BaseLayer extends EventEmitter * -> SceneContainer 1.* * -> LayerContainer 1.* */ - public setContainer(container: Container) { + public setContainer(container: Container, sceneContainer: Container) { this.container = container; + this.sceneContainer = sceneContainer; } public getContainer() { diff --git a/packages/layers/src/imagetile/index.ts b/packages/layers/src/imagetile/index.ts new file mode 100644 index 0000000000..037c2bfe79 --- /dev/null +++ b/packages/layers/src/imagetile/index.ts @@ -0,0 +1,38 @@ +import BaseLayer from '../core/BaseLayer'; +import ImageTileModels, { ImageTileModelType } from './models/index'; +interface IImageLayerStyleOptions { + opacity: number; +} +export default class ImageTileLayer extends BaseLayer { + public type: string = 'ImageTileLayer'; + public buildModels() { + const modelType = this.getModelType(); + this.layerModel = new ImageTileModels[modelType](this); + this.models = this.layerModel.initModels(); + } + public rebuildModels() { + this.models = this.layerModel.buildModels(); + } + protected getConfigSchema() { + return { + properties: { + opacity: { + type: 'number', + minimum: 0, + maximum: 1, + }, + }, + }; + } + protected getDefaultConfig() { + const type = this.getModelType(); + const defaultConfig = { + imageTile: {}, + }; + return defaultConfig[type]; + } + + protected getModelType(): ImageTileModelType { + return 'imageTile'; + } +} diff --git a/packages/layers/src/imagetile/models/imagetile.ts b/packages/layers/src/imagetile/models/imagetile.ts new file mode 100644 index 0000000000..a9b7a02a2d --- /dev/null +++ b/packages/layers/src/imagetile/models/imagetile.ts @@ -0,0 +1,128 @@ +import { + AttributeType, + gl, + IEncodeFeature, + ILayer, + ILayerPlugin, + IModel, + IModelUniform, + IRasterParserDataItem, + IStyleAttributeService, + ITexture2D, + lazyInject, + TYPES, +} from '@antv/l7-core'; +import BaseModel from '../../core/BaseModel'; +import { RasterImageTriangulation } from '../../core/triangulation'; +import ImageTileFrag from './shaders/imagetile_frag.glsl'; +import ImageTileVert from './shaders/imagetile_vert.glsl'; + +import Tile from '../utils/Tile'; + +interface IImageLayerStyleOptions { + resolution: string; + maxSourceZoom: number; +} + +export default class ImageTileModel extends BaseModel { + public tileLayer: any; + public getUninforms(): IModelUniform { + return {}; + } + + // 临时的瓦片测试方法 + public tile() { + const [WS, EN] = this.mapService.getBounds(); + const NE = { lng: EN[0], lat: EN[1] }; + const SW = { lng: WS[0], lat: WS[1] }; + this.tileLayer.calCurrentTiles({ + NE, + SW, + tileCenter: this.mapService.getCenter(), + currentZoom: this.mapService.getZoom(), + minSourceZoom: this.mapService.getMinZoom(), + minZoom: this.mapService.getMinZoom(), + maxZoom: this.mapService.getMaxZoom(), + }); + } + + public initModels() { + // TODO: 瓦片组件默认在最下层 + this.layer.zIndex = -999; + const { + resolution = 'low', + maxSourceZoom = 17, + } = this.layer.getLayerConfig() as IImageLayerStyleOptions; + const source = this.layer.getSource(); + // 当存在 url 的时候生效 + if (source.data.tileurl) { + this.tileLayer = new Tile({ + url: source.data.tileurl, + layerService: this.layerService, + layer: this.layer, + resolution, + maxSourceZoom, + // Tip: 当前为 default + crstype: 'epsg3857', + }); + + this.tile(); + let t = new Date().getTime(); + this.mapService.on('mapchange', () => { + const newT = new Date().getTime(); + const cutT = newT - t; + t = newT; + if (cutT < 16) { + return; + } + this.tile(); + }); + } + + return [ + this.layer.buildLayerModel({ + moduleName: 'ImageTileLayer', + vertexShader: ImageTileVert, + fragmentShader: ImageTileFrag, + triangulation: RasterImageTriangulation, + primitive: gl.TRIANGLES, + depth: { enable: false }, + blend: this.getBlend(), + }), + ]; + } + + public clearModels() { + this.tileLayer.removeTiles(); + } + + public buildModels() { + return this.initModels(); + } + + protected registerBuiltinAttributes() { + // point layer size; + this.styleAttributeService.registerStyleAttribute({ + name: 'uv', + type: AttributeType.Attribute, + descriptor: { + name: 'a_Uv', + buffer: { + // give the WebGL driver a hint that this buffer may change + usage: gl.DYNAMIC_DRAW, + data: [], + type: gl.FLOAT, + }, + size: 2, + update: ( + feature: IEncodeFeature, + featureIdx: number, + vertex: number[], + attributeIdx: number, + ) => { + return [vertex[3], vertex[4]]; + }, + }, + }); + } +} diff --git a/packages/layers/src/imagetile/models/index.ts b/packages/layers/src/imagetile/models/index.ts new file mode 100644 index 0000000000..aa1c9aff08 --- /dev/null +++ b/packages/layers/src/imagetile/models/index.ts @@ -0,0 +1,8 @@ +import ImageTileModel from './imagetile'; +export type ImageTileModelType = 'imageTile'; + +const ImageTileModels: { [key in ImageTileModelType]: any } = { + imageTile: ImageTileModel, +}; + +export default ImageTileModels; diff --git a/packages/layers/src/imagetile/models/shaders/imagetile_frag.glsl b/packages/layers/src/imagetile/models/shaders/imagetile_frag.glsl new file mode 100644 index 0000000000..de00ecaab4 --- /dev/null +++ b/packages/layers/src/imagetile/models/shaders/imagetile_frag.glsl @@ -0,0 +1,4 @@ +precision mediump float; +void main() { + gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); +} diff --git a/packages/layers/src/imagetile/models/shaders/imagetile_vert.glsl b/packages/layers/src/imagetile/models/shaders/imagetile_vert.glsl new file mode 100644 index 0000000000..98d0be5916 --- /dev/null +++ b/packages/layers/src/imagetile/models/shaders/imagetile_vert.glsl @@ -0,0 +1,14 @@ +precision highp float; +uniform mat4 u_ModelMatrix; +uniform mat4 u_Mvp; +attribute vec3 a_Position; +#pragma include "projection" +void main() { + vec4 project_pos = project_position(vec4(a_Position, 1.0)); + // gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy,0., 1.0)); + if(u_CoordinateSystem == COORDINATE_SYSTEM_P20_2) { // gaode2.x + gl_Position = u_Mvp * (vec4(project_pos.xy,0., 1.0)); + } else { + gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy,0., 1.0)); + } +} diff --git a/packages/layers/src/imagetile/shaders/image_frag.glsl b/packages/layers/src/imagetile/shaders/image_frag.glsl new file mode 100644 index 0000000000..ed617c17e3 --- /dev/null +++ b/packages/layers/src/imagetile/shaders/image_frag.glsl @@ -0,0 +1,9 @@ +precision mediump float; +uniform float u_opacity: 1.0; +uniform sampler2D u_texture; +varying vec2 v_texCoord; +void main() { + vec4 color = texture2D(u_texture,vec2(v_texCoord.x,v_texCoord.y)); + gl_FragColor = color; + gl_FragColor.a *= u_opacity; +} diff --git a/packages/layers/src/imagetile/shaders/image_vert.glsl b/packages/layers/src/imagetile/shaders/image_vert.glsl new file mode 100644 index 0000000000..fd96cd725c --- /dev/null +++ b/packages/layers/src/imagetile/shaders/image_vert.glsl @@ -0,0 +1,17 @@ +precision highp float; +uniform mat4 u_ModelMatrix; +uniform mat4 u_Mvp; +attribute vec3 a_Position; +attribute vec2 a_Uv; +varying vec2 v_texCoord; +#pragma include "projection" +void main() { + v_texCoord = a_Uv; + vec4 project_pos = project_position(vec4(a_Position, 1.0)); + // gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy,0., 1.0)); + if(u_CoordinateSystem == COORDINATE_SYSTEM_P20_2) { // gaode2.x + gl_Position = u_Mvp * (vec4(project_pos.xy,0., 1.0)); + } else { + gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy,0., 1.0)); + } +} diff --git a/packages/layers/src/imagetile/utils/ImageTile.ts b/packages/layers/src/imagetile/utils/ImageTile.ts new file mode 100644 index 0000000000..2688c063bf --- /dev/null +++ b/packages/layers/src/imagetile/utils/ImageTile.ts @@ -0,0 +1,95 @@ +import { LngLatBounds, toBounds, toLngLatBounds } from '@antv/geo-coord'; +import { Container } from 'inversify'; +import ImageLayer from '../../image'; + +interface IUrlParams { + x: number; + y: number; + z: number; + s?: string; +} + +const r2d = 180 / Math.PI; +const tileURLRegex = /\{([zxy])\}/g; + +export default class ImageTile { + public tile: number[]; // 当前图片瓦片的索引 + public name: string; + public imageLayer: any; + constructor( + key: string, + url: string, + container: Container, + sceneContainer: Container, + ) { + this.name = key; + this.tile = key.split('_').map((v) => Number(v)); + + const urlParams = { + x: this.tile[0], + y: this.tile[1], + z: this.tile[2], + }; + const imageSrc = this.getTileURL(urlParams, url); + + const lnglatBounds = this.tileLnglatBounds(this.tile); + const west = lnglatBounds.getWest(); + const south = lnglatBounds.getSouth(); + const east = lnglatBounds.getEast(); + const north = lnglatBounds.getNorth(); + + const imageLayer = new ImageLayer({}); + imageLayer.source( + // 'https://gw.alipayobjects.com/zos/rmsportal/FnHFeFklTzKDdUESRNDv.jpg', + imageSrc, + { + parser: { + type: 'image', + // extent: [121.168, 30.2828, 121.384, 30.4219], + extent: [west, south, east, north], + }, + }, + ); + + imageLayer.setContainer(container, sceneContainer); + imageLayer.init(); + + this.imageLayer = imageLayer; + } + + public destroy() { + this.imageLayer.clearModels(); + this.imageLayer.destroy(); + } + + public getTileURL(urlParams: IUrlParams, path: string) { + if (!urlParams.s) { + // Default to a random choice of a, b or c + urlParams.s = String.fromCharCode(97 + Math.floor(Math.random() * 3)); + } + + tileURLRegex.lastIndex = 0; + return path.replace(tileURLRegex, (value, key: any) => { + // @ts-ignore + return urlParams[key]; + }); + } + + // Get tile bounds in WGS84 coordinates + public tileLnglatBounds(tile: number[]) { + const e = this.tile2lng(tile[0] + 1, tile[2]); + const w = this.tile2lng(tile[0], tile[2]); + const s = this.tile2lat(tile[1] + 1, tile[2]); + const n = this.tile2lat(tile[1], tile[2]); + return toLngLatBounds([w, n], [e, s]); + } + + public tile2lng(x: number, z: number) { + return (x / Math.pow(2, z)) * 360 - 180; + } + + public tile2lat(y: number, z: number) { + const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z); + return r2d * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); + } +} diff --git a/packages/layers/src/imagetile/utils/Tile.ts b/packages/layers/src/imagetile/utils/Tile.ts new file mode 100644 index 0000000000..d44598f324 --- /dev/null +++ b/packages/layers/src/imagetile/utils/Tile.ts @@ -0,0 +1,359 @@ +import { Bounds, GeoCoordinates, Point, toLngLat } from '@antv/geo-coord'; +import { + createLayerContainer, + ILayer, + ILayerService, + ILngLat, +} from '@antv/l7-core'; +import { Container } from 'inversify'; + +import ImageTile from './ImageTile'; +import TileCache from './tileCache'; + +// Tip: 瓦片地图的存储上限 +const CacheLimit = 30; + +export default class Tile { + public tileList: any = {}; + public tileCache: any; + + public updateTileList: any[]; + public tileZoom: number; + public noPruneRange: any; + public url: string; + public resolution: number; + public maxSourceZoom: number; + public crstype: string; + public currentCrs: any; + + public layerService: ILayerService; + public layer: ILayer; + constructor(props: any) { + this.layerService = props.layerService; + this.layer = props.layer; + this.url = props.url; + this.resolution = props.resolution === 'low' ? -1 : 0; + this.maxSourceZoom = props.maxSourceZoom; + this.crstype = props.crstype; + + this.currentCrs = new GeoCoordinates.default({ + start: { x: 0, y: 0 }, + end: { x: 0, y: 0 }, + projection: this.crstype, + }).crs as any; + + this.destroyTile = this.destroyTile.bind(this); + this.tileCache = new TileCache(CacheLimit, this.destroyTile); + + this.updateTileList = []; + + this.removeTiles = this.removeTiles.bind(this); + } + + public calCurrentTiles(oprions: any) { + const { + NE, + SW, + tileCenter, + currentZoom, + minSourceZoom, + minZoom, + maxZoom, + } = oprions; + // TODO: 当前瓦片的层级要比地图底图的层级低 + if (currentZoom >= this.maxSourceZoom) { + return; + } + const zoom = Math.floor(currentZoom) + this.resolution; + + this.tileZoom = zoom > this.maxSourceZoom ? this.maxSourceZoom : zoom; + + if ( + currentZoom < minZoom || + currentZoom >= maxZoom || + currentZoom < minSourceZoom + ) { + this.removeTiles(); + return; + } + + this.updateTileList = []; + + // 计算瓦片中心 + const centerPoint = this.currentCrs.lngLatToPoint( + toLngLat(tileCenter.lng, tileCenter.lat), + this.tileZoom, + ); + const centerXY = centerPoint.divideBy(256).floor(); + + const pixelBounds = this.getPixelBounds( + NE, + SW, + tileCenter, + this.tileZoom, + this.currentCrs, + ); // 计算像素范围 + const tileRange = this.pxBoundsToTileRange(pixelBounds); // 计算瓦片范围 + + const margin = 4; + + this.noPruneRange = new Bounds( + tileRange.getBottomLeft().subtract([margin, -margin]), + tileRange.getTopRight().add([margin, -margin]), + ); + + // T: isFinite(n: number) 用于检测 n 是否无穷大 + if ( + !( + isFinite(tileRange.min.x) && + isFinite(tileRange.min.y) && + isFinite(tileRange.max.x) && + isFinite(tileRange.max.y) + ) + ) { + throw new Error('Attempted to load an infinite number of tiles'); + } + + // 根据视野判断新增的瓦片索引 + for (let j = tileRange.min.y; j <= tileRange.max.y; j++) { + for (let i = tileRange.min.x; i <= tileRange.max.x; i++) { + const coords = [i, j, this.tileZoom]; + const tile = this.tileList[coords.join('_')]; + if (tile) { + tile.current = true; + } else { + this.tileList[coords.join('_')] = { + current: true, + coords, + }; + this.updateTileList.push(coords); + } + } + } + + // 瓦片列表排序 + this.updateTileList.sort((a: any, b: any) => { + const tile1 = a; + const tile2 = b; + const d1 = + Math.pow(tile1[0] * 1 - centerXY.x, 2) + + Math.pow(tile1[1] * 1 - centerXY.y, 2); + const d2 = + Math.pow(tile2[0] * 1 - centerXY.x, 2) + + Math.pow(tile2[1] * 1 - centerXY.y, 2); + return d1 - d2; + }); + + this.pruneTiles(); + this.updateTileList.forEach((coords: any) => { + const key = coords.join('_'); + if (this.tileList[key].current) { + this.requestTile(key); + } + }); + } + + public pxBoundsToTileRange(pixelBounds: any) { + return new Bounds( + pixelBounds.min.divideBy(256).floor(), + pixelBounds.max + .divideBy(256) + .ceil() + .subtract([1, 1]), + ); + } + + public getPixelBounds( + NE: ILngLat, + SW: ILngLat, + tileCenter: ILngLat, + tileZoom: number, + crs: any, + ) { + const zoom = tileZoom; + const NEPoint = crs.lngLatToPoint(toLngLat(NE.lng, NE.lat), zoom); + const SWPoint = crs.lngLatToPoint(toLngLat(SW.lng, SW.lat), zoom); + const centerPoint = crs.lngLatToPoint( + toLngLat(tileCenter.lng, tileCenter.lat), + zoom, + ); + const topHeight = centerPoint.y - NEPoint.y; + const bottomHeight = SWPoint.y - centerPoint.y; + // 跨日界线的情况 + let leftWidth; + let rightWidth; + if (tileCenter.lng - NE.lng > 0 || tileCenter.lng - SW.lng < 0) { + const width = + ((Math.pow(2, zoom) * 256) / 360) * (180 - NE.lng) + + ((Math.pow(2, zoom) * 256) / 360) * (SW.lng + 180); + if (tileCenter.lng - NE.lng > 0) { + // 日界线在右侧 + leftWidth = + ((Math.pow(2, zoom) * 256) / 360) * (tileCenter.lng - NE.lng); + rightWidth = width - leftWidth; + } else { + rightWidth = + ((Math.pow(2, zoom) * 256) / 360) * (SW.lng - tileCenter.lng); + leftWidth = width - rightWidth; + } + } else { + // 不跨日界线 + leftWidth = ((Math.pow(2, zoom) * 256) / 360) * (tileCenter.lng - SW.lng); + rightWidth = + ((Math.pow(2, zoom) * 256) / 360) * (NE.lng - tileCenter.lng); + } + const pixelBounds = new Bounds( + centerPoint.subtract(leftWidth, topHeight), + centerPoint.add(rightWidth, bottomHeight), + ); + return pixelBounds; + } + + public pruneTiles() { + Object.keys(this.tileList).map((key) => { + const c = this.tileList[key].coords; + // 如果不是同一个缩放层级,则将瓦片设为不显示 + if ( + c[2] !== this.tileZoom || + !this.noPruneRange.contains(new Point(c[0], c[1])) + ) { + this.tileList[key].current = false; + } + }); + + Object.keys(this.tileList).map((key) => { + const tile = this.tileList[key]; + tile.retain = tile.current; + }); + + Object.keys(this.tileList).map((key) => { + const tile = this.tileList[key]; + if (tile.current && !tile.active) { + const [x, y, z] = key.split('_').map((v) => Number(v)); + + if (!this.retainParent(x, y, z, z - 5)) { + this.retainChildren(x, y, z, z + 2); + } + } + }); + + this.removeOutTiles(); + } + + public requestTile(key: string) { + const t = this.tileList[key]; + if (!t) { + return; + } + let tile = this.tileCache.getTile(key); + if (!tile) { + const container = createLayerContainer( + this.layer.sceneContainer as Container, + ); + tile = new ImageTile( + key, + this.url, + container, + this.layer.sceneContainer as Container, + ); + tile.name = key; + + t.current = true; + t.retain = true; + t.active = true; + + // 往 imageTileLayer 中添加子图层 + this.layer.layerChildren.push(tile.imageLayer); + + this.tileCache.setTile(tile, key); + + this.pruneTiles(); + this.layerService.renderLayers(); + } else { + // Tip: show 方法就是将相应的瓦片图片添加到渲染队列 + tile.imageLayer.show(); + t.current = true; + t.retain = true; + t.active = true; + + this.pruneTiles(); + } + } + + public retainParent(x: number, y: number, z: number, minZoom: number): any { + const x2 = Math.floor(x / 2); + const y2 = Math.floor(y / 2); + const z2 = z - 1; + const tile = this.tileList[[x2, y2, z2].join('_')]; + if (tile && tile.active) { + tile.retain = true; + return true; + } else if (tile && tile.loaded) { + tile.retain = true; + } + if (z2 > minZoom) { + return this.retainParent(x2, y2, z2, minZoom); + } + return false; + } + + public retainChildren(x: number, y: number, z: number, maxZoom: number) { + for (let i = 2 * x; i < 2 * x + 2; i++) { + for (let j = 2 * y; j < 2 * y + 2; j++) { + const key = [i, j, z + 1].join('_'); + const tile = this.tileList[key]; + if (tile && tile.active) { + tile.retain = true; + continue; + } else if (tile && tile.loaded) { + tile.retain = true; + } + + if (z + 1 < maxZoom) { + this.retainChildren(i, j, z + 1, maxZoom); + } + } + } + } + + public destroyTile(tile: any) { + const layerIndex = this.layer.layerChildren.indexOf(tile.imageLayer); + if (layerIndex > -1) { + this.layer.layerChildren.splice(layerIndex, 1); + } + + tile.imageLayer.emit('remove', null); + tile.imageLayer.destroy(); + this.layerService.renderLayers(); + + // 清除 tileCache 中的存储 相当于 tileCache.setTile(tile, null) + tile = null; + } + + public removeOutTiles() { + for (const key in this.tileList) { + if (!this.tileList[key].retain) { + // Tip: 不需要显示的瓦片对象 + const tile = this.tileCache.getTile(key); + // Tip: 若是网格对象存在 + if (tile) { + // Tip: hide 方法就是将相应的瓦片图片从渲染队列中剔除 + tile.imageLayer.hide(); + } + delete this.tileList[key]; + } + } + } + + public removeTiles() { + this.layer.layerChildren.forEach((layer: any) => { + layer.emit('remove', null); + layer.destroy(); + }); + + this.layer.layerChildren = []; + this.layerService.renderLayers(); + this.tileList = {}; + this.tileCache.destory(); + } +} diff --git a/packages/layers/src/imagetile/utils/lruCache.ts b/packages/layers/src/imagetile/utils/lruCache.ts new file mode 100644 index 0000000000..4a67a769a6 --- /dev/null +++ b/packages/layers/src/imagetile/utils/lruCache.ts @@ -0,0 +1,80 @@ +/** + * LRU Cache class with limit + * + * Update order for each get/set operation + * Delete oldest when reach given limit + */ + +export default class LRUCache { + public limit: number; + public order: any[]; + public cache: any; + public destroy: any; + constructor(limit = 50, destroy = () => '') { + this.limit = limit; + this.destroy = destroy; + this.order = []; + this.clear(); + } + + public clear() { + this.order.forEach((key: any) => { + this.delete(key); + }); + this.cache = {}; + // access/update order, first item is oldest, last item is newest + this.order = []; + } + + public get(key: string) { + const value = this.cache[key]; + if (value) { + // update order + this.deleteOrder(key); + this.appendOrder(key); + } + return value; + } + + public set(key: string, value: any) { + if (!this.cache[key]) { + // if reach limit, delete the oldest + if (Object.keys(this.cache).length === this.limit) { + this.delete(this.order[0]); + } + + this.cache[key] = value; + this.appendOrder(key); + } else { + // if found in cache, delete the old one, insert new one to the first of list + this.delete(key); + + this.cache[key] = value; + this.appendOrder(key); + } + } + + public delete(key: string) { + const value = this.cache[key]; + if (value) { + this.deleteCache(key); + this.deleteOrder(key); + this.destroy(value, key); + } + } + + public deleteCache(key: string) { + delete this.cache[key]; + } + + public deleteOrder(key: string) { + const index = this.order.findIndex((o) => o === key); + if (index >= 0) { + this.order.splice(index, 1); + } + } + + public appendOrder(key: string) { + this.order.push(key); + } +} diff --git a/packages/layers/src/imagetile/utils/tileCache.ts b/packages/layers/src/imagetile/utils/tileCache.ts new file mode 100644 index 0000000000..ecaf557b75 --- /dev/null +++ b/packages/layers/src/imagetile/utils/tileCache.ts @@ -0,0 +1,24 @@ +import LRUCache from './lruCache'; +export default class TileCache { + public cache: any; + constructor(limit = 50, tileDestroy: any) { + this.cache = new LRUCache(limit, tileDestroy); + } + + public getTile(key: string) { + return this.cache.get(key); + } + + public setTile(tile: any, key: string) { + this.cache.set(key, tile); + } + public destory() { + this.cache.clear(); + } + public setNeedUpdate() { + this.cache.order.forEach((key: string) => { + const tile = this.cache.get(key); + tile.needUpdate = true; + }); + } +} diff --git a/packages/layers/src/index.ts b/packages/layers/src/index.ts index cc13eb3431..d33ad6b240 100644 --- a/packages/layers/src/index.ts +++ b/packages/layers/src/index.ts @@ -4,6 +4,7 @@ import BaseLayer from './core/BaseLayer'; import './glsl.d'; import HeatmapLayer from './heatmap'; import ImageLayer from './image'; +import ImageTileLayer from './imagetile'; import LineLayer from './line/index'; import PointLayer from './point'; import PolygonLayer from './polygon'; @@ -25,6 +26,7 @@ import RegisterStyleAttributePlugin from './plugins/RegisterStyleAttributePlugin import ShaderUniformPlugin from './plugins/ShaderUniformPlugin'; import UpdateModelPlugin from './plugins/UpdateModelPlugin'; import UpdateStyleAttributePlugin from './plugins/UpdateStyleAttributePlugin'; + /** * 校验传入参数配置项的正确性 * @see /dev-docs/ConfigSchemaValidation.md @@ -137,6 +139,7 @@ export { LineLayer, CityBuildingLayer, ImageLayer, + ImageTileLayer, RasterLayer, HeatmapLayer, EarthLayer, diff --git a/packages/maps/src/amap/map.ts b/packages/maps/src/amap/map.ts index d87e516277..a4617729d0 100644 --- a/packages/maps/src/amap/map.ts +++ b/packages/maps/src/amap/map.ts @@ -459,6 +459,8 @@ export default class AMapService position, } = e.camera; const { lng, lat } = this.getCenter(); + // Tip: 触发地图变化事件 + this.emit('mapchange'); if (this.cameraChangedCallback) { // resync viewport // console.log('cameraHeight', height) diff --git a/packages/maps/src/amap2/map.ts b/packages/maps/src/amap2/map.ts index 5da810d0c6..1ff15c130b 100644 --- a/packages/maps/src/amap2/map.ts +++ b/packages/maps/src/amap2/map.ts @@ -558,6 +558,8 @@ export default class AMapService // left, right, bottom, top // @ts-ignore } = this.map.customCoords?.getCameraParams(); + // Tip: 统一触发地图变化事件 + this.emit('mapchange'); // // @ts-ignore // console.log('this.map.customCoords.getCameraParams()', this.map.customCoords.getCameraParams()) // const { left, right, bottom, top, near, far, position } = this.map.customCoords.getCameraParams(); @@ -620,6 +622,9 @@ export default class AMapService // left, right, bottom, top // @ts-ignore } = this.map.customCoords.getCameraParams(); + // Tip: 统一触发地图变化事件 + this.emit('mapchange'); + const { zoom } = e; // @ts-ignore const center = this.map.customCoords.getCenter() as [number, number]; diff --git a/packages/maps/src/earth/map.ts b/packages/maps/src/earth/map.ts index 5443116b02..7c25a28352 100644 --- a/packages/maps/src/earth/map.ts +++ b/packages/maps/src/earth/map.ts @@ -337,6 +337,8 @@ export default class L7EarthService implements IEarthService { } private handleCameraChanged = (e: any) => { + // Tip: 统一触发地图变化事件 + this.emit('mapchange'); const DELAY_TIME = 2000; this.handleCameraChanging = true; if (this.handleCameraTimer) { diff --git a/packages/maps/src/map/map.ts b/packages/maps/src/map/map.ts index 12adbb581c..2c11cfbafd 100644 --- a/packages/maps/src/map/map.ts +++ b/packages/maps/src/map/map.ts @@ -320,7 +320,8 @@ export default class L7MapService implements IMapService { private handleCameraChanged = () => { const { lat, lng } = this.map.getCenter(); const { offsetCoordinate = true } = this.config; - + // Tip: 统一触发地图变化事件 + this.emit('mapchange'); // resync this.viewport.syncWithMapCamera({ bearing: this.map.getBearing(), diff --git a/packages/maps/src/mapbox/map.ts b/packages/maps/src/mapbox/map.ts index ef3b9fda61..166b618485 100644 --- a/packages/maps/src/mapbox/map.ts +++ b/packages/maps/src/mapbox/map.ts @@ -403,7 +403,8 @@ export default class MapboxService private handleCameraChanged = () => { // @see https://github.com/mapbox/mapbox-gl-js/issues/2572 const { lat, lng } = this.map.getCenter().wrap(); - + // Tip: 统一触发地图变化事件 + this.emit('mapchange'); // resync this.viewport.syncWithMapCamera({ bearing: this.map.getBearing(), diff --git a/packages/scene/src/index.ts b/packages/scene/src/index.ts index 5ae3aaed4f..51f6361c38 100644 --- a/packages/scene/src/index.ts +++ b/packages/scene/src/index.ts @@ -162,7 +162,7 @@ class Scene // 为当前图层创建一个容器 // TODO: 初始化的时候设置 容器 const layerContainer = createLayerContainer(this.container); - layer.setContainer(layerContainer); + layer.setContainer(layerContainer, this.container); this.sceneService.addLayer(layer); } @@ -178,8 +178,8 @@ class Scene return this.layerService.getLayerByName(name); } - public removeLayer(layer: ILayer): void { - this.layerService.remove(layer); + public removeLayer(layer: ILayer, parentLayer?: ILayer): void { + this.layerService.remove(layer, parentLayer); } public removeAllLayer(): void { diff --git a/packages/source/src/source.ts b/packages/source/src/source.ts index cd83ddc4ef..ad2774bb2f 100644 --- a/packages/source/src/source.ts +++ b/packages/source/src/source.ts @@ -207,6 +207,14 @@ export default class Source extends EventEmitter implements ISource { private excuteParser(): void { const parser = this.parser; const type: string = parser.type || 'geojson'; + // TODO: 图片瓦片地图组件只需要使用 url 参数 + if (type === 'imagetile') { + this.data = { + tileurl: this.originData, + dataArray: [], + }; + return; + } const sourceParser = getParser(type); this.data = sourceParser(this.originData, parser); // 计算范围 diff --git a/stories/3D_Model/Components/aspace.tsx b/stories/3D_Model/Components/aspace.tsx index 8cd07ea3fb..5adaddfcf6 100644 --- a/stories/3D_Model/Components/aspace.tsx +++ b/stories/3D_Model/Components/aspace.tsx @@ -137,7 +137,6 @@ export default class Aspace extends React.Component { let r = heightData[i * 4]; let g = heightData[i * 4 + 1]; let b = heightData[i * 4 + 2]; - let h = -10000.0 + (r * 255.0 * 256.0 * 256.0 + g * 255.0 * 256.0 + b * 255.0) * @@ -168,7 +167,6 @@ export default class Aspace extends React.Component { const antModel = gltf.scene; layer.adjustMeshToMap(antModel); layer.setMeshScale(antModel, 20, 20, 20); - layer.setObjectLngLat( antModel, [center.lng - 0.002, center.lat], @@ -178,13 +176,9 @@ export default class Aspace extends React.Component { const animations = gltf.animations; if (animations && animations.length) { const mixer = new THREE.AnimationMixer(antModel); - const animation = animations[1]; - const action = mixer.clipAction(animation); - action.play(); - layer.addAnimateMixer(mixer); } antModel.rotation.y = Math.PI; @@ -358,8 +352,7 @@ export default class Aspace extends React.Component { currentView.pitch = scene.getPitch(); currentView.zoom = scene.getZoom(); }); - // @ts-ignore - scene?.map?.on('camerachange', (e: any) => { + scene.getMapService().on('mapchange', (e: any) => { // @ts-ignore currentCamera = threeJSLayer.threeRenderService.getRenderCamera(); currentView.pitch = scene.getPitch(); diff --git a/stories/Map/components/amap2demo_imageTileLayer.tsx b/stories/Map/components/amap2demo_imageTileLayer.tsx new file mode 100644 index 0000000000..4b15fe519b --- /dev/null +++ b/stories/Map/components/amap2demo_imageTileLayer.tsx @@ -0,0 +1,93 @@ +// @ts-ignore +import { ImageTileLayer, Scene, PointLayer } from '@antv/l7'; +import { GaodeMap, GaodeMapV2, Map, Mapbox } from '@antv/l7-maps'; +import * as React from 'react'; + +export default class Amap2demo_imageTileLayer extends React.Component { + // @ts-ignore + private scene: Scene; + + public componentWillUnmount() { + this.scene.destroy(); + } + + public async componentDidMount() { + const scene = new Scene({ + id: 'map', + map: new Map({ + center: [121.268, 30.3628], + pitch: 0, + style: 'normal', + zoom: 10, + viewMode: '3D', + }), + }); + this.scene = scene; + + let originData = [ + { + lng: 121.107846, + lat: 30.267069, + }, + { + lng: 121.107, + lat: 30.267069, + }, + { + lng: 121.107846, + lat: 30.26718, + }, + ]; + + scene.on('loaded', () => { + const layer = new ImageTileLayer({}); + layer + .source( + 'http://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}', + { + parser: { + type: 'imagetile', + }, + }, + ) + .style({ + resolution: 'low', // low height + // resolution: 'height' + maxSourceZoom: 17, + }); + + let pointlayer = new PointLayer() + .source(originData, { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }) + .shape('circle') + .color('rgba(255, 0, 0, 1.0)') + .size(10) + .active(true); + + scene.addLayer(pointlayer); + scene.addLayer(layer); + }); + } + + public render() { + return ( + <> +
+ + ); + } +} diff --git a/stories/Map/map.stories.tsx b/stories/Map/map.stories.tsx index 8a9889811e..482cae2c61 100644 --- a/stories/Map/map.stories.tsx +++ b/stories/Map/map.stories.tsx @@ -38,6 +38,7 @@ import Amap2demo_heatmap_hexagon_world from './components/amap2demo_heatmap_hexa import Amap2demo_heatmap_grid from "./components/amap2demo_heatmap_grid" import Amap2demo_imageLayer from "./components/amap2demo_imagelayer" +import Amap2demo_imageTileLayer from "./components/amap2demo_imageTileLayer" import Amap2demo_rasterLayer from "./components/amap2demo_rasterlayer" @@ -99,7 +100,10 @@ storiesOf('地图方法', module) .add('高德地图2.0 heatmap3D/hexagon', () => ) .add('高德地图2.0 heatmap/hexagon/world', () => ) .add('高德地图2.0 heatmap3D/grid', () => ) + .add('高德地图2.0 imageLayer', () => ) + .add('高德地图2.0 imageTileLayer', () => ) + .add('高德地图2.0 rasterLayer', () => ) .add('高德地图2.0 citybuildLayer', () => )