fix:修复事件名丢失 (#1393)

* chore: rename sourceUpdate -> update

* chore: remove useless code

* feat: 修改初始化 mask 的 逻辑

Co-authored-by: shihui <yiqianyao.yqy@alibaba-inc.com>
This commit is contained in:
YiQianYao 2022-10-14 15:47:45 +08:00 committed by GitHub
parent 849c095ea8
commit 6754bf52d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 162 additions and 334 deletions

View File

@ -76,7 +76,7 @@ export default () => {
// thetaOffset: 0.5, // thetaOffset: 0.5,
}); });
source.on('sourceUpdate', () => { source.on('update', () => {
// console.log(scene.getMapService().version) // console.log(scene.getMapService().version)
// const midPoints = lineAtOffset(source, 0.3, 'arc', 'offset'); // const midPoints = lineAtOffset(source, 0.3, 'arc', 'offset');
const midPoints = lineAtOffset(source, { const midPoints = lineAtOffset(source, {

View File

@ -78,7 +78,7 @@ export default () => {
.color('#f00') .color('#f00')
.style({}); .style({});
source.on('sourceUpdate', () => { source.on('update', () => {
console.log(source); console.log(source);
const midPoints = lineAtOffset(source, { const midPoints = lineAtOffset(source, {
offset: 0.1, offset: 0.1,

View File

@ -77,7 +77,7 @@ export default () => {
thetaOffset: 0.5, thetaOffset: 0.5,
}); });
source.on('sourceUpdate', () => { source.on('update', () => {
const midPoints = lineAtOffset(source, { const midPoints = lineAtOffset(source, {
offset: 0.1, offset: 0.1,
shape: 'arc', shape: 'arc',

View File

@ -1,5 +1,5 @@
// @ts-ignore // @ts-ignore
import { Scene, HeatmapLayer } from '@antv/l7'; import { Scene, HeatmapLayer, PointLayer } from '@antv/l7';
// @ts-ignore // @ts-ignore
import { GaodeMap } from '@antv/l7-maps'; import { GaodeMap } from '@antv/l7-maps';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
@ -49,18 +49,81 @@ export default () => {
], ],
}; };
const point = new PointLayer({
// mask: true,
// maskInside: false,
// maskfence: maskData,
// maskColor: '#fff',
// maskOpacity: 0.5,
}).source([{
lng:120, lat:30
}], {
parser: {
type: 'json',
x: 'lng',
y: 'lat'
}
})
.shape('circle')
.size(20)
.color('#f00');
scene.addLayer(point)
scene.on('loaded', () => { scene.on('loaded', () => {
// const point = new PointLayer({
// mask: true,
// maskInside: false,
// maskfence: maskData,
// maskColor: '#fff',
// maskOpacity: 0.5,
// }).source([{
// lng:120, lat:30
// }], {
// parser: {
// type: 'json',
// x: 'lng',
// y: 'lat'
// }
// })
// .shape('circle')
// .size(20)
// .color('#f00');
// scene.addLayer(point)
// point.on('update', ( ) =>point.renderLayers())
fetch( fetch(
'https://gw.alipayobjects.com/os/basement_prod/d3564b06-670f-46ea-8edb-842f7010a7c6.json', 'https://gw.alipayobjects.com/os/basement_prod/d3564b06-670f-46ea-8edb-842f7010a7c6.json',
) )
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
// const point = new PointLayer({
// // mask: true,
// // maskInside: false,
// // maskfence: maskData,
// // maskColor: '#fff',
// // maskOpacity: 0.5,
// }).source([{
// lng:130, lat:30
// }], {
// parser: {
// type: 'json',
// x: 'lng',
// y: 'lat'
// }
// })
// .shape('circle')
// .size(20)
// .color('#f00');
// scene.addLayer(point)
const heatmapLayer = new HeatmapLayer({ const heatmapLayer = new HeatmapLayer({
mask: true, // mask: true,
maskInside: false, // maskInside: false,
maskfence: maskData, // maskfence: maskData,
maskColor: '#fff', // maskColor: '#fff',
maskOpacity: 0.5, // maskOpacity: 0.5,
}) })
.source(data) .source(data)
.shape('heatmap3D') // heatmap3D heatmap .shape('heatmap3D') // heatmap3D heatmap
@ -81,7 +144,14 @@ export default () => {
positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0], positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0],
}, },
}); });
scene.addLayer(heatmapLayer); scene.addLayer(heatmapLayer);
// scene.render()
// setTimeout(() =>{
// scene.render();
// }, 1000)
// console.log('rrr')
}); });
}); });
}, []); }, []);

View File

@ -42,6 +42,30 @@ export default () => {
], ],
}; };
const data2 = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
testOpacity: 0.8,
},
geometry: {
type: 'Polygon',
coordinates: [
[
[113.8623046875, 30.031055426540206],
[115.3232421875, 30.031055426540206],
[115.3232421875, 31.090574094954192],
[113.8623046875, 31.090574094954192],
[113.8623046875, 30.031055426540206],
],
],
},
},
],
};
const layer = new PolygonLayer({}) const layer = new PolygonLayer({})
.source(data) .source(data)
@ -53,6 +77,16 @@ export default () => {
scene.on('loaded', () => { scene.on('loaded', () => {
scene.addLayer(layer); scene.addLayer(layer);
setTimeout(() =>{
layer.setData(data2)
}, 200)
layer.on('mousemove', () =>{
console.log('mousemove')
})
layer.on('unmousemove', () =>{
console.log('unmousemove')
})
}); });
}, []); }, []);
return ( return (

View File

@ -79,20 +79,3 @@ export default {
如果传入了错误的配置项则会在控制台给出提示。 如果传入了错误的配置项则会在控制台给出提示。
## Layer 子类配置项 Schema
Layer 子类可以通过重载 `getConfigSchema()` 方法定义自身的特有属性。例如 `PolygonLayer` 需要定义透明度:
```javascript
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
```

View File

@ -106,27 +106,6 @@ protected setupShaders() {
} }
``` ```
### 配置项校验服务
开发者不需要显式调用该服务。
Layer 子类可以通过重载 `getConfigSchema()` 方法定义自身的特有属性。例如 `PolygonLayer` 需要定义透明度,详见[ConfigSchemaValidation 使用方法](ConfigSchemaValidation.md)
```typescript
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
```
以上就是供开发者使用的常见全局服务,下面我们将介绍场景容器及其内部服务。
## Scene 容器 ## Scene 容器
场景可以承载多个图层,与地图底图一一对应。每个场景都有自己独立的容器确保多个场景间服务不会互相干扰,同时继承全局容器以便访问全局服务。容器内服务包括: 场景可以承载多个图层,与地图底图一一对应。每个场景都有自己独立的容器确保多个场景间服务不会互相干扰,同时继承全局容器以便访问全局服务。容器内服务包括:

View File

@ -29,17 +29,6 @@ export default class GeometryLayer extends BaseLayer<
}); });
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
const type = this.getModelType(); const type = this.getModelType();
const defaultConfig = { const defaultConfig = {

View File

@ -146,18 +146,6 @@ export default class BillBoardModel extends BaseModel {
} }
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected registerBuiltinAttributes() { protected registerBuiltinAttributes() {
this.styleAttributeService.registerStyleAttribute({ this.styleAttributeService.registerStyleAttribute({
name: 'extrude', name: 'extrude',

View File

@ -356,18 +356,6 @@ export default class PlaneModel extends BaseModel {
} }
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected registerBuiltinAttributes() { protected registerBuiltinAttributes() {
// point layer size; // point layer size;
this.styleAttributeService.registerStyleAttribute({ this.styleAttributeService.registerStyleAttribute({

View File

@ -223,18 +223,6 @@ export default class SpriteModel extends BaseModel {
} }
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected registerBuiltinAttributes() { protected registerBuiltinAttributes() {
return ''; return '';
} }

View File

@ -45,17 +45,6 @@ export default class CanvasLayer extends BaseLayer<ICanvasLayerStyleOptions> {
return this; return this;
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
const type = this.getModelType(); const type = this.getModelType();
const defaultConfig = { const defaultConfig = {

View File

@ -19,17 +19,6 @@ export default class CityBuildingLayer extends BaseLayer {
time: t, time: t,
}); });
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getModelType(): string { protected getModelType(): string {
return 'citybuilding'; return 'citybuilding';

View File

@ -969,7 +969,7 @@ export default class BaseLayer<ChildLayerStyleOptions = {}>
this.hooks.beforeDestroy.call(); this.hooks.beforeDestroy.call();
// 清除sources事件 // 清除sources事件
this.layerSource.off('sourceUpdate', this.sourceEvent); this.layerSource.off('update', this.sourceEvent);
this.multiPassRenderer?.destroy(); this.multiPassRenderer?.destroy();
@ -1028,7 +1028,7 @@ export default class BaseLayer<ChildLayerStyleOptions = {}>
public setSource(source: Source) { public setSource(source: Source) {
// 清除旧 sources 事件 // 清除旧 sources 事件
if (this.layerSource) { if (this.layerSource) {
this.layerSource.off('sourceUpdate', this.sourceEvent); this.layerSource.off('update', this.sourceEvent);
} }
this.layerSource = source; this.layerSource = source;
@ -1043,17 +1043,13 @@ export default class BaseLayer<ChildLayerStyleOptions = {}>
if (this.layerSource.inited) { if (this.layerSource.inited) {
this.sourceEvent(); this.sourceEvent();
} }
// this.layerSource.inited 为 true sourceUpdate 事件不会再触发 // this.layerSource.inited 为 true update 事件不会再触发
this.layerSource.on('sourceUpdate', () => { this.layerSource.on('update', () => {
if (this.coordCenter === undefined) { if (this.coordCenter === undefined) {
const layerCenter = this.layerSource.center; const layerCenter = this.layerSource.center;
this.coordCenter = layerCenter; this.coordCenter = layerCenter;
this.mapService.setCoordCenter && this.mapService?.setCoordCenter &&
this.mapService.setCoordCenter(layerCenter); this.mapService.setCoordCenter(layerCenter);
// // @ts-ignore
// this.mapService.map.customCoords.setCenter(layerCenter);
// // @ts-ignore
// this.mapService.setCustomCoordCenter(layerCenter);
} }
this.sourceEvent(); this.sourceEvent();
}); });
@ -1358,10 +1354,6 @@ export default class BaseLayer<ChildLayerStyleOptions = {}>
console.warn('empty fn'); console.warn('empty fn');
} }
protected getConfigSchema() {
throw new Error('Method not implemented.');
}
protected getModelType(): unknown { protected getModelType(): unknown {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

View File

@ -49,18 +49,6 @@ export default class HeatMapLayer extends BaseLayer<IHeatMapLayerStyleOptions> {
} }
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getModelType(): HeatMapModelType { protected getModelType(): HeatMapModelType {
const shapeAttribute = this.styleAttributeService.getLayerStyleAttribute( const shapeAttribute = this.styleAttributeService.getLayerStyleAttribute(
'shape', 'shape',

View File

@ -15,17 +15,7 @@ export default class ImageLayer extends BaseLayer<IImageLayerStyleOptions> {
this.dispatchModelLoad(models); this.dispatchModelLoad(models);
}); });
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
const type = this.getModelType(); const type = this.getModelType();
const defaultConfig = { const defaultConfig = {

View File

@ -120,18 +120,6 @@ export default class ImageDataModel extends BaseModel {
this.initModels(callbackModel); this.initModels(callbackModel);
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected registerBuiltinAttributes() { protected registerBuiltinAttributes() {
this.styleAttributeService.registerStyleAttribute({ this.styleAttributeService.registerStyleAttribute({
name: 'uv', name: 'uv',

View File

@ -97,18 +97,6 @@ export default class ImageModel extends BaseModel {
this.initModels(callbackModel); this.initModels(callbackModel);
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected registerBuiltinAttributes() { protected registerBuiltinAttributes() {
this.styleAttributeService.registerStyleAttribute({ this.styleAttributeService.registerStyleAttribute({
name: 'uv', name: 'uv',

View File

@ -105,18 +105,6 @@ export default class ImageDataModel extends BaseModel {
this.initModels(callbackModel); this.initModels(callbackModel);
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected registerBuiltinAttributes() { protected registerBuiltinAttributes() {
this.styleAttributeService.registerStyleAttribute({ this.styleAttributeService.registerStyleAttribute({
name: 'uv', name: 'uv',

View File

@ -39,17 +39,6 @@ export default class LineLayer extends BaseLayer<ILineLayerStyleOptions> {
}); });
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
const type = this.getModelType(); const type = this.getModelType();
const defaultConfig = { const defaultConfig = {

View File

@ -113,17 +113,6 @@ export default class MaskLayer extends BaseLayer<IMaskLayerStyleOptions> {
this.dispatchModelLoad(models); this.dispatchModelLoad(models);
}); });
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getModelType(): MaskModelType { protected getModelType(): MaskModelType {
const parserType = this.layerSource.getParserType(); const parserType = this.layerSource.getParserType();

View File

@ -37,7 +37,7 @@ export default class DataMappingPlugin implements ILayerPlugin {
if (source.inited) { if (source.inited) {
this.generateMaping(layer, { styleAttributeService }); this.generateMaping(layer, { styleAttributeService });
} else { } else {
source.once('sourceUpdate', () => { source.once('update', () => {
this.generateMaping(layer, { styleAttributeService }); this.generateMaping(layer, { styleAttributeService });
}); });
} }
@ -49,7 +49,7 @@ export default class DataMappingPlugin implements ILayerPlugin {
if (source.inited) { if (source.inited) {
this.generateMaping(layer, { styleAttributeService }); this.generateMaping(layer, { styleAttributeService });
} else { } else {
source.once('sourceUpdate', () => { source.once('update', () => {
this.generateMaping(layer, { styleAttributeService }); this.generateMaping(layer, { styleAttributeService });
}); });
} }

View File

@ -20,7 +20,7 @@ export default class DataSourcePlugin implements ILayerPlugin {
if (source.inited) { if (source.inited) {
this.updateClusterData(layer); this.updateClusterData(layer);
} else { } else {
source.once('sourceUpdate', () => { source.once('update', () => {
this.updateClusterData(layer); this.updateClusterData(layer);
}); });
} }

View File

@ -47,7 +47,7 @@ export default class FeatureScalePlugin implements ILayerPlugin {
if (source.inited) { if (source.inited) {
callback(source.data); callback(source.data);
} else { } else {
source.once('sourceUpdate', () => { source.once('update', () => {
callback(source.data); callback(source.data);
}); });
} }

View File

@ -40,7 +40,7 @@ export default class LayerModelPlugin implements ILayerPlugin {
if (source.inited) { if (source.inited) {
this.prepareLayerModel(layer); this.prepareLayerModel(layer);
} else { } else {
source.once('sourceUpdate', () => { source.once('update', () => {
this.prepareLayerModel(layer); this.prepareLayerModel(layer);
}); });
} }

View File

@ -55,17 +55,7 @@ export default class PointLayer extends BaseLayer<IPointLayerStyleOptions> {
} }
return 'normal'; return 'normal';
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
const type = this.getModelType(); const type = this.getModelType();
const defaultConfig = { const defaultConfig = {

View File

@ -26,17 +26,6 @@ export default class PolygonLayer extends BaseLayer<IPolygonLayerStyleOptions> {
this.dispatchModelLoad(models); this.dispatchModelLoad(models);
}); });
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getModelType(): PolygonModelType { protected getModelType(): PolygonModelType {
const parserType = this.layerSource.getParserType(); const parserType = this.layerSource.getParserType();

View File

@ -15,17 +15,7 @@ export default class RaterLayer extends BaseLayer<IRasterLayerStyleOptions> {
this.dispatchModelLoad(models); this.dispatchModelLoad(models);
}); });
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
const type = this.getModelType(); const type = this.getModelType();
const defaultConfig = { const defaultConfig = {

View File

@ -66,17 +66,6 @@ export default class RasterLayer extends BaseLayer<IRasterLayerStyleOptions> {
return this; return this;
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
private buildRasterModel() { private buildRasterModel() {
const source = this.getSource(); const source = this.getSource();

View File

@ -28,17 +28,6 @@ export default class RasterTiffLayer extends BaseLayer<
return RasterModel; return RasterModel;
} }
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
return {}; return {};

View File

@ -215,18 +215,6 @@ export default class VectorLayer extends BaseLayer<
} }
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
return {}; return {};
} }

View File

@ -23,17 +23,7 @@ export default class WindLayer extends BaseLayer<IWindLayerStyleOptions> {
return this; return this;
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected getDefaultConfig() { protected getDefaultConfig() {
const type = this.getModelType(); const type = this.getModelType();
const defaultConfig = { const defaultConfig = {

View File

@ -158,18 +158,6 @@ export default class WindModel extends BaseModel {
this.wind?.destroy(); this.wind?.destroy();
} }
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected registerBuiltinAttributes() { protected registerBuiltinAttributes() {
// point layer size; // point layer size;
this.styleAttributeService.registerStyleAttribute({ this.styleAttributeService.registerStyleAttribute({

View File

@ -175,32 +175,39 @@ class Scene
layer.setContainer(layerContainer, this.container); layer.setContainer(layerContainer, this.container);
this.sceneService.addLayer(layer); this.sceneService.addLayer(layer);
const layerConfig = layer.getLayerConfig(); // mask 在 scene loaded 之后执行
if (layerConfig) { if (layer.inited) {
// 若 layer 未初始化成功,则 layerConfig 为 undefined scene loaded 尚未执行完成) const maskInstance = this.initMask(layer);
const { this.addMask(maskInstance as ILayer, layer.id);
mask,
maskfence,
maskColor = '#000',
maskOpacity = 0,
} = layerConfig;
if (mask && maskfence) {
const maskInstance = new MaskLayer()
.source(maskfence)
.shape('fill')
.style({
color: maskColor,
opacity: maskOpacity,
});
this.addMask(maskInstance, layer.id);
}
} else { } else {
console.warn('addLayer should run after scene loaded!'); layer.on('inited', () => {
const maskInstance = this.initMask(layer);
this.addMask(maskInstance as ILayer, layer.id);
})
} }
} }
public initMask(layer: ILayer) {
const {
mask,
maskfence,
maskColor = '#000',
maskOpacity = 0,
} = layer.getLayerConfig();
if(!mask) return undefined;
const maskInstance = new MaskLayer()
.source(maskfence)
.shape('fill')
.style({
color: maskColor,
opacity: maskOpacity,
});
return maskInstance;
}
public addMask(mask: ILayer, layerId: string) { public addMask(mask: ILayer, layerId: string) {
if(!mask) return;
const parent = this.getLayer(layerId); const parent = this.getLayer(layerId);
if (parent) { if (parent) {
const layerContainer = createLayerContainer(this.container); const layerContainer = createLayerContainer(this.container);

View File

@ -234,11 +234,10 @@ export default class Source extends EventEmitter implements ISource {
} }
private init() { private init() {
// this.hooks.init.call(this);
this.inited = false; this.inited = false;
this.handleData().then(() => { this.handleData().then(() => {
this.inited = true; this.inited = true;
this.emit('sourceUpdate'); this.emit('update');
}); });
} }

View File

@ -23,7 +23,7 @@ export function lineAtOffsetAsyc(source: Source, option: ILineAtOffset) {
if (source.inited) { if (source.inited) {
resolve(lineAtOffset(source, option)); resolve(lineAtOffset(source, option));
} else { } else {
source.once('sourceUpdate', () => { source.once('update', () => {
resolve(lineAtOffset(source, option)); resolve(lineAtOffset(source, option));
}); });
} }

View File

@ -96,7 +96,7 @@ export default class Demo extends React.Component {
}, },
}, },
); );
source.on('sourceUpdate', () => { source.on('update', () => {
const layer = new PointLayer(); const layer = new PointLayer();
layer.source(source); layer.source(source);
layer layer
@ -309,7 +309,7 @@ export default class Demo extends React.Component {
.shape('circle') .shape('circle')
.size(10) .size(10)
.color('#f00'); .color('#f00');
source.on('sourceUpdate', () => { source.on('update', () => {
layer.source(source); layer.source(source);
}); });
this.scene.addLayer(layer); this.scene.addLayer(layer);
@ -356,7 +356,7 @@ export default class Demo extends React.Component {
.shape('circle') .shape('circle')
.size(10) .size(10)
.color('#f00'); .color('#f00');
source.on('sourceUpdate', () => { source.on('update', () => {
setTimeout(() => { setTimeout(() => {
layer.source(source); layer.source(source);
}, 2000); }, 2000);
@ -390,7 +390,7 @@ export default class Demo extends React.Component {
.shape('circle') .shape('circle')
.size(10) .size(10)
.color('#f00'); .color('#f00');
source.on('sourceUpdate', () => { source.on('update', () => {
setTimeout(() => { setTimeout(() => {
layer.source(source); layer.source(source);
}, 2000); }, 2000);