feat: clean code

This commit is contained in:
shihui 2023-01-04 14:56:55 +08:00
parent 8250a729c4
commit 00d70cdf2a
1131 changed files with 69 additions and 138711 deletions

View File

@ -21,39 +21,10 @@ export default defineConfig({
},
mode: 'site',
navs: [
{
title: 'bugs',
path: '/bugs',
},
{
title: '特性',
path: '/features',
},
{
title: '图库',
path: '/gallery',
},
{
title: '瓦片',
path: '/tile',
},
{
title: '栅格',
path: '/raster',
},
{
title: '组件',
path: '/component',
},
{
title: '绘制组件',
path: '/draw',
},
{
title: 'GitHub',
path: 'https://github.com/antvis/L7',
},
],
esbuild: false,
chainWebpack: (memo, { env, webpack, createCSSRule }) => {

View File

@ -16,27 +16,7 @@ export default () => {
}),
});
const pointLayer = new PointLayer({})
.source([{
x: 120, y: 30
}], {
parser: {
type: 'json',
x: 'x',
y: 'y',
},
})
.shape('circle')
.size(16)
.active(true)
.select({
color: 'red',
})
.color('#f00')
.style({
opacity: 1,
strokeWidth: 0,
stroke: '#fff',
});
scene.on('loaded', () => {
scene.addLayer(pointLayer);

View File

@ -1,81 +0,0 @@
# ConfigSchemaValidation 设计
用户在使用 L7 的 Scene/Layer API 时,由于参数配置项众多难免会误传。需要在运行时通过校验提前发现并给出友好的提示。
另外由于 L7 允许用户自定义 Layer 与 LayerPlugin规范化参数配置项也能提升易用性和质量。
这方面 Webpack 做的很好,使用 [schema-utils](https://github.com/webpack/schema-utils) 基于 JSON Schema 对 Plugin 和 Loader 进行校验。如果传入了错误的配置项,会给出友好的提示:
```
Invalid configuration object. MyPlugin has been initialised using a configuration object that does not match the API schema.
- configuration.optionName should be a integer.
```
和 Webpack 一样,我们也选择 [ajv](https://github.com/epoberezkin/ajv) 作为 JSON Schema 校验器。
目前我们只在 Layer 初始阶段进行校验,一旦校验失败会中断后续初始化插件的处理,并在控制台给出校验失败信息。后续需要在属性更新时同样进行校验。
## 地图参数校验
当用户传入地图参数时,需要进行校验:
```javascript
// l7-core/services/config/mapConfigSchema.ts
export default {
properties: {
// 地图缩放等级
zoom: {
type: 'number',
minimum: 0,
maximum: 20,
},
// 地图中心点
center: {
item: {
type: 'number',
},
maxItems: 2,
minItems: 2,
},
// 仰角
pitch: {
type: 'number',
},
},
};
```
## Layer 基类配置项 Schema
目前在基类中我们声明了如下属性及其对应的校验规则:
```javascript
export default {
properties: {
// 开启拾取
enablePicking: {
type: 'boolean',
},
// 开启高亮
enableHighlight: {
type: 'boolean',
},
// 高亮颜色:例如 [0, 0, 1, 1] 或者 '#ffffff'
highlightColor: {
oneOf: [
{
type: 'array',
items: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
{
type: 'string',
},
],
},
},
};
```
如果传入了错误的配置项则会在控制台给出提示。

View File

@ -1,164 +0,0 @@
# IoC 容器、依赖注入与服务说明
在面向对象编程领域,[SOLID](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)) 、[“组合优于继承”](https://en.wikipedia.org/wiki/Composition_over_inheritance) 都是经典的设计原则。
IoC(Inversion of Control) 控制反转这种设计模式将对象的创建销毁、依赖关系交给容器处理,是以上设计原则的一种经典实践。其中它的一种实现 DI(Dependency Injection) 即依赖注入在工程领域应用十分广泛(下图来自 [Dependency-Injection-in-practice-CodeCAMP.pdf](http://www.mono.hr/Pdf/Dependency-Injection-in-practice-CodeCAMP.pdf)),最著名的当属 Spring
![](./screenshots/di-containers.png)
而在 JavaScript 领域,[Angular](https://angular.io/guide/dependency-injection)、[NestJS](https://docs.nestjs.com/fundamentals/custom-providers) 也都实现了自己的 IoC 容器。
L7 选择了 [InversifyJS](https://github.com/inversify/InversifyJS/blob/master/wiki/oo_design.md) 作为轻量的 IoC 容器,统一管理各类复杂的服务,实现松耦合的代码结构,同时具有以下收益:
* 提供高扩展性。
* 支持地图底图高德、Mapbox切换
* 支持渲染引擎替换
* 插件化
* 便于测试。测试用例中替换渲染引擎服务为基于 headless-gl 的渲染服务。
下图清晰的展示了切换引擎和底图时均不会影响核心代码:
![](./screenshots/packages.png)
# 多层次容器
L7 需要支持多场景(Scene),每个场景中又包含了多个图层(Layer)。不同的服务可能隶属全局、Scene 和 Layer因此对于容器也有层次化的要求。
试想如果我们只有一个全局容器,其中绑定的所有服务自然也都成了全局服务,在多场景下(页面中一个高德地图、一个 Mapbox销毁高德地图的渲染服务将影响到 Mapbox 的展示。
下图为 L7 的四个独立场景(两个高德、两个 MapboxDEMO 展示效果,它们应该是能互不干扰运行的:
![](./screenshots/multi-scene.png)
在 Angular 中也有[分层容器](https://angular.io/guide/hierarchical-dependency-injection)的应用。L7 使用的是 InversifyJS 提供的[层次化依赖注入功能](https://github.com/inversify/InversifyJS/blob/master/wiki/hierarchical_di.md)。
容器层次关系及数目如下:
```bash
RootContainer 1
-> SceneContainer 1.*
-> LayerContainer 1.*
```
其中每种容器包含不同类型的服务,这些服务有的是单例,有的是工厂方法。子容器应该能访问父容器中绑定的服务,即如果 RootContainer 已经绑定了全局日志服务SceneContainer 不需要重复绑定也能注入。
下面详细介绍下每种容器中的服务及其 API在自定义图层、自定义插件以及自定义后处理效果中都可以方便地使用这些服务。
## 全局容器
一些全局性服务不需要用户手动创建,也无需显式销毁。我们在全局容器中完成一次性的绑定,后续在所有场景、图层中都可以让容器注入这些服务的单例。类似 Angular 中的 [root ModuleInjector](https://angular.io/guide/hierarchical-dependency-injection#moduleinjector)。
例如日志、Shader 模块化服务应该是全局性的单例,我们在 `RootContainer` 完成依赖声明:
```typescript
// 在根容器中绑定日志服务为单例
rootContainer
.bind<ILogService>(TYPES.ILogService)
.to(LogService)
.inSingletonScope();
```
目前 L7 中全局性服务说明如下:
| 服务名称 | 类型 | 说明 |
| -------- | --- | --------- |
| logger | 全局服务 | 在控制台输出信息 |
* 日志服务。
* Shader 模块化服务。提供基本的 GLSL 模块化服务,基于字符串替换实现。
* 配置项校验服务。[详见](./ConfigSchemaValidation.md)
### Shader 模块化服务
通过 `shaderModuleService` 引用,可使用 API 如下:
| 方法名 | 参数 | 返回值 | 说明 |
| -------- | ------------- | --------- | --------- |
| registerModule | `(moduleName: string, moduleParams: IModuleParams)` | 无 | 使用模块名和参数注册 GLSL 模块,其中 `IModuleParams` 格式见下面 |
| getModule | `(moduleName: string)` | `IModuleParams` | 根据模块名获取编译后的 GLSL 模块 |
GLSL 模块参数如下:
```typescript
interface IModuleParams {
vs: string; // vertex shader 字符串
fs: string; // fragment shader 字符串
uniforms?: { // 可选uniforms
[key: string]: IUniform;
};
}
```
我们以自定义后处理效果场景为例,完整教程见[自定义后处理效果](自定义后处理效果.md)
```typescript
protected setupShaders() {
// 使用 Shader 服务注册 GLSL 模块
this.shaderModuleService.registerModule('dotScreenEffect', {
vs: this.quad, // Vertex Shader 固定
fs: ``, // 暂时省略,在下一小节中详细介绍
});
// 使用 Shader 服务获取编译后的 GLSL 模块
const { vs, fs, uniforms } = this.shaderModuleService.getModule('dotScreenEffect');
// 使用渲染器服务获取视口尺寸
const { width, height } = this.rendererService.getViewportSize();
return {
vs,
fs,
uniforms: {
...uniforms,
u_ViewportSize: [width, height],
},
};
}
```
## Scene 容器
场景可以承载多个图层,与地图底图一一对应。每个场景都有自己独立的容器确保多个场景间服务不会互相干扰,同时继承全局容器以便访问全局服务。容器内服务包括:
* 地图底图服务。每个场景有一个对应的地图底图。
* 渲染引擎服务。由于依赖 WebGL 上下文,基于 `regl` 实现。
* 图层管理服务。管理场景中所有的图层,负责图层的创建、销毁。
* PostProcessingPass。内置常用的后处理效果。
### 地图底图服务
兼容 Mapbox 和高德,开发者可以获取当前地图的状态、调用地图相机动作(缩放、平移、旋转)。
通过 `mapService` 引用。
常用地图状态获取方法如下:
| 方法名 | 参数 | 返回值 | 说明 |
| -------- | ------------- | --------- | --------- |
| getSize | 无 | `[number, number]` | 获取地图尺寸(像素单位) |
| getZoom | 无 | `number` | 获取当前地图缩放等级,以 Mapbox 为准 |
| getCenter | 无 | `{lng: number; lat: number}` | 获取当前地图中心点经纬度 |
| getPitch | 无 | `number` | 获取当前地图仰角 |
| getRotation | 无 | `number` | 获取当前地图逆时针旋转角度 |
| getBounds | 无 | `[[number, number], [number, number]]` | 获取当前地图可视区域 `[西南角、东北角]` |
⚠️对于一些地图属性将采用兼容性处理。
* 缩放等级,差异表现在:
1. 取值范围。高德缩放等级范围 `[3, 18]`,而 Mapbox 为 `[0, 20]`
2. 高德 `3` 缩放等级对应 Mapbox `2` 缩放等级。考虑兼容性,`getZoom()` 将返回 Mapbox 定义等级。
* 旋转角度。高德返回地图顺时针旋转角度Mapbox 返回逆时针旋转角度。考虑兼容性,`getRotation()` 将返回地图逆时针旋转角度。
除了获取地图状态,还可以控制地图进行一些相机动作。
### [WIP]渲染引擎服务
目前 L7 使用 [regl](https://github.com/regl-project/regl),但开发者不需要关心底层 WebGL 渲染引擎实现,即使后续更换了其他引擎,我们也将保持服务接口的稳定。
通过 `rendererService` 引用。
### 图层管理服务
开发者不需要显式调用。用于管理场景中所有的图层,负责图层的创建、销毁。
## Layer 容器
每个图层有独立的容器,同时继承自所属场景容器,自然也可以访问全局服务。
* 样式管理服务。
* MultiPassRenderer 服务。详见[MultiPassRenderer 说明](./MultiPassRenderer.md)
## 参考资料
* [动态依赖注入](https://github.com/inversify/InversifyJS/issues/1088)

View File

@ -1,167 +0,0 @@
# MultiPassRenderer 实现
每个 Layer 渲染时都需要经历多个流程,从最简单的清屏、拾取到各种各样的后处理。我们希望把复杂渲染流程中每个步骤都抽象出来,让 L7 内部以及用户能够方便的扩展,进行渲染流程的自定义。其中的每一个步骤称作 Pass负责串联调用各个 Pass 的渲染器称作 MultiPassRenderer。
![](./screenshots/blurpass.png)
## 接口设计
目前我们将 Pass 分成两类:
1. 渲染相关。例如 ClearPass、RenderPass、PickingPass、ShadowPass
2. 后处理相关。例如 CopyPass、BlurPass
```typescript
export enum PassType {
Normal = 'normal',
PostProcessing = 'post-processing',
}
```
每个 Pass 定义两个生命周期节点,初始化和渲染,并将当前 Layer 作为参数传入。因此 Pass 中可以访问 Layer 上的属性及方法:
```typescript
export interface IPass {
getType(): PassType;
init(layer: ILayer): void;
render(layer: ILayer): void;
}
```
其中后处理相关的 Pass 比较特殊,例如最后一个 PostProcessingPass 需要自动切换 renderTarget 为屏幕:
```typescript
export interface IPostProcessingPass extends IPass {
setRenderToScreen(renderToScreen: boolean): void;
isEnabled(): boolean;
setEnabled(enabled: boolean): void;
}
```
具体实现依赖 `@antv/l7-renderer` 实现,目前使用 regl 实现 IFramebuffer 等接口。
## 内置 Pass
目前我们仅对外开放 PostProcessing 后处理相关 Pass 的配置。在 L7 内部我们使用如下流程:
```
ClearPass -> RenderPass -> [ ...其他后处理 Pass ] -> CopyPass
```
目前各内置 Pass 说明如下:
| Pass 名称 | 类型 | 参数 | 说明 |
| -------- | --- | ------------- | --------- |
| ClearPass | normal | 无 | 清除 framebufferclearColor 为 [0, 0, 0, 0] |
| RenderPass | normal | 无 | 渲染到 framebuffer作为后续后处理的输入 |
| PickingPass | normal | 无 | 负责拾取,[详见](./PixelPickingEngine.md) |
| TAAPass | normal | 无 | [详见](./TAA.md) |
| CopyPass | post-processing | 无 | 作为后处理最后一个 Pass负责拷贝 framebuffer 到屏幕输出 |
剩余后处理效果见最后一节。
后续待实现 Pass 如下:
- [ ] ShadowPass 负责生成 shadowMap供 PCF、CSM 等实时阴影技术使用
## 使用方法
在每个 Layer 中,通过 `enableMultiPassRenderer` 开启之后,可以配置各个 Pass 的参数。配置方法类似 babel 插件:
```typescript
const layer = new PolygonLayer({
enableMultiPassRenderer: true,
passes: [
'blurH', // 使用 BlurHPass
[
'blurV', // 使用 BlurVPass
{
blurRadius: 20, // 设置模糊半径
},
],
],
});
```
## 内置后处理效果
参考了 [glfx](https://github.com/evanw/glfx.js) 中的一些常用图像处理效果。可以按照名称引用,顺序决定了各个效果的应用次序。例如我们想依次应用噪声和模糊效果:
```typescript
const layer = new PolygonLayer({
passes: [
[
'noise', // 使用 NoisePass
{
amount: 0.5,
},
]
'blurH', // 使用 BlurHPass
'blurV', // 使用 BlurVPass
],
});
```
下面详细介绍各个后处理效果及其参数,在 DEMO 中也可以通过 GUI 任意调节参数。
### 高斯模糊
采用 [高斯模糊 blur9](https://github.com/Jam3/glsl-fast-gaussian-blur/blob/master/9.glsl)。
名称:`blurH/blurV`
参数:
* `blurRadius` 水平/垂直方向模糊半径,默认值为 `8.0`
效果如下:
![](./screenshots/blurpass.png)
### ColorHalftone
CMYK halftone 效果
名称:`colorHalftone`
参数:
* `angle` pattern 旋转角度,默认值为 0
* `size` pattern 大小,默认值为 8
* `center` `[x, y]` pattern 的中心,默认值为 `[0, 0]`
效果如下:
![](./screenshots/halftone.png)
### 噪声
噪声效果。
名称:`noise`
参数:
* `amount` 噪声程度,范围 `[0, 1]`,默认值为 `0.5`
效果如下:
![](./screenshots/noise.png)
### 六边形像素化处理
六边形像素化处理。
名称:`hexagonalPixelate`
参数:
* `scale` 六边形大小,默认值为 `10`
* `center` `[x, y]` pattern 的中心,默认值为 `[0.5, 0.5]`
效果如下:
![](./screenshots/hexagonalPixelate.png)
### Sepia
Sepia 颜色映射。
名称:`sepia`
参数:
* `amount` 程度,范围 `[0, 1]`,默认值为 `0.5`
效果如下:
![](./screenshots/sepia.png)

View File

@ -1,151 +0,0 @@
# PixelPickingEngine 设计
在地图交互中除了地图底图本身提供的平移、旋转、缩放、flyTo 等相机动作,最常用的就是信息要素的拾取以及后续的高亮了。
3D 引擎常用的拾取技术通常有两种RayPicking 和 PixelPicking。前者从鼠标点击处沿着投影方向发射一根射线通过包围盒碰撞检测获取到接触到的第一个对象后续就可以进行选中对象的高亮甚至是跟随移动了以上运算均在 CPU 侧完成。
但是在 L7 的场景中,海量数据在同一个 Geometry 中,无法计算每个要素的包围盒,因此在 GPU 侧完成的 PixelPicking 更加适合。
作为拾取引擎 PixelPickingEngine除了实现内置基本的拾取 Pass最重要的是提供友好易用的 API覆盖以下常见场景
* 基本的拾取场景,用户只需要开启 Layer 拾取功能并设置高亮颜色即可。
* 拾取后展示特定 UI 组件的场景,用户需要监听事件,在回调中使用上述拾取对象完成组件展示。
* 更灵活的联动场景,用户可以不依赖 L7 内置的事件监听机制,直接拾取并高亮指定点/区域包含的要素。
本文会依次介绍:
* PixelPicking 原理
* 使用方法
* 拾取对象结构
* 拾取 API 的使用方法
* 开启/关闭拾取
* 设置高亮颜色
* 展示自定义 UI 组件
* 在自定义 Layer 中使用
## PixelPicking 原理
在执行时机方面,基于 [MultiPassRenderer](./MultiPassRenderer.md) 的设计,拾取发生在实际渲染之前:
```
ClearPass -> PixelPickingPass -> RenderPass -> [ ...其他后处理 Pass ] -> CopyPass
```
PixelPickingPass 分解步骤如下:
1. `ENCODE` 阶段。逐要素编码idx -> color传入 attributes 渲染 Layer 到纹理。
2. 获取鼠标在视口中的位置。由于目前 L7 与地图结合的方案为双 Canvas 而非共享 WebGL Context事件监听注册在地图底图上。
3. 读取纹理在指定位置的颜色进行解码color -> idx),查找对应要素,作为 Layer `onHover/onClick` 回调参数传入。
4. `HIGHLIGHT` 阶段(可选)。将待高亮要素对应的颜色传入 Vertex Shader 用于每个 Vertex 判断自身是否被选中,如果被选中,在 Fragment Shader 中将高亮颜色与计算颜色混合。
## 使用方法
### 拾取对象结构定义
拾取对象结构定义如下:
| 参数名 | 类型 | 说明 |
| -------- | --- | ------------- |
| x | `number` | 鼠标位置在视口空间 x 坐标,取值范围 `[0, viewportWidth]` |
| y | `number` | 鼠标位置在视口空间 y 坐标,取值范围 `[0, viewportHeight]` |
| lnglat | `{ lng: number; lat: number; }` | 鼠标位置经纬度坐标 |
| feature | `object` | GeoJSON feature 属性 |
### API
对于基本的拾取场景,用户只需要开启 Layer 拾取功能并设置高亮颜色即可。
而对于拾取后展示特定 UI 组件的场景,用户需要监听事件,在回调中使用上述拾取对象完成组件展示。
最后,对于更灵活的联动场景,用户可以不依赖 L7 内置的事件监听机制,直接拾取并高亮指定点/区域包含的要素。
#### 禁用/开启拾取
并不是所有 Layer 都需要拾取(例如文本渲染 Layer通过 `enablePicking` 关闭可以跳过该阶段,减少不必要的渲染开销:
```typescript
const layer = new PolygonLayer({
enablePicking: false, // 关闭拾取
});
```
L7 默认开启拾取。
#### 设置高亮颜色
如果一个 Layer 开启了拾取,我们可以通过 `highlightColor` 设置高亮颜色:
```typescript
const layer = new PolygonLayer({
enablePicking: true, // 开启拾取
enableHighlight: true, // 开启高亮
highlightColor: [0, 0, 1, 1], // 设置高亮颜色为蓝色
});
```
#### 展示自定义 UI 组件
监听 Layer 上的 `hover/mousemove` 事件就可以得到拾取对象,然后通过对象中包含的位置以及原始数据信息,就可以使用 L7 内置或者自定义 UI 组件展示:
```typescript
layer.on('hover', ({ x, y, lnglat, feature }) => {
// 展示 UI 组件
});
layer.on('mousemove', ({ x, y, lnglat, feature }) => {
// 同上
});
```
除了基于事件监听,还可以通过 Layer 的构造函数传入 `onHover` 回调,在后续 Layer 对应的 react 组件中也可以以这种方式使用:
```typescript
const layer = new PolygonLayer({
enablePicking: true,
onHover: ({ x, y, lnglat, feature }) => {
// 展示 UI 组件
},
});
```
#### 直接调用拾取引擎方法
除了默认在地图上交互完成拾取,在与其他系统进行联动时,脱离了地图交互,仍需要具备拾取指定点/区域内包含要素的能力。
```typescript
anotherSystem.on('hover', ({ x, y }) => {
layer.pick({
x,
y,
});
});
```
⚠️目前只支持拾取视口中一个点所在的要素,未来可以实现拾取指定区域内的全部要素。
### 自定义 Layer 中的拾取
用户实现自定义 Layer 时,必然需要实现 Vertex/Fragment Shader。如果也想使用拾取功能就需要在 Shader 中引入拾取模块,方法如下。
在 Vertex Shader 中引入 `picking` 模块。关于 L7 Shader 的模块化设计,[详见]()。
```glsl
// mylayer.vert.glsl
#pragma include "picking"
void main() {
setPickingColor(customPickingColors);
}
```
在 Fragment Shader 中
```glsl
// mylayer.frag.glsl
#pragma include "picking"
void main() {
// 必须在末尾,保证后续不会再对 gl_FragColor 进行修改
gl_FragColor = filterPickingColor(gl_FragColor);
}
```
其中涉及 `picking` 模块方法说明如下:
| 方法名 | 应用 shader | 说明 |
| -------- | --- | ------------- |
| `setPickingColor` | `vertex` | 比较自身颜色编码与高亮颜色,判断是否被选中,传递结果给 fragment |
| `filterPickingColor` | `fragment` | 当前 fragment 被选中则使用高亮颜色混合,否则直接输出原始计算结果 |
## 参考资料
* [Deck.gl 交互文档](https://deck.gl/#/documentation/developer-guide/adding-interactivity)
* [Deck.gl Picking 实现](https://deck.gl/#/documentation/developer-guide/writing-custom-layers/picking)
* 「Interactive.Computer.Graphics.Top.Down.Approach - 3.9 Picking」

View File

@ -1,118 +0,0 @@
# 在地理场景中应用 TAA
## 问题背景
关于走样产生的原因以及常用的反走样手段,可以参考「知乎 - 反走样技术(一):几何反走样」[🔗](https://zhuanlan.zhihu.com/p/28800047)。
我之前也简单总结了下 SSAA、MLAA/SMAA、FXAA 等反走样技术的实现细节。
其中 MSAA 作为浏览器内置实现,开发者使用起来很简单:
> 相对于着色走样人眼对几何走样更敏感。MSAA 的原理很简单,它仍然把一个像素划分为若干个子采样点,但是相较于 SSAA每个子采样点的颜色值完全依赖于对应像素的颜色值进行简单的复制该子采样点位于当前像素光栅化结果的覆盖范围内不进行单独计算。此外它的做法和 SSAA 相同。由于 MSAA 拥有硬件支持,相对开销比较小,又能很好地解决几何走样问题,在游戏中应用非常广泛(我们在游戏画质选项中常看到的 4x/8x/16x 抗锯齿一般说的就是 MSAA 的子采样点数量分别为4/8/16个
下图为 4x MSAA 采样点示意:
![](./screenshots/MSAA.png)
在 Mapbox 中左图未开启 MSAA 而右图选择开启,观察立方体边缘可以发现明显的几何走样:相关 [ISSUE](https://github.com/mapbox/mapbox-gl-js/pull/8474)。
![](./screenshots/mapbox-MSAA.png)
但是 MSAA 存在一些限制:
* WebGL1 不支持对 FBO 进行,因此开启 post-processing 后处理时 MSAA 就失效了。当然 WebGL2 支持 🔗。
* 即使开启,浏览器在某些情况下也不保证应用 🔗。
因此在需要后处理的场景中(例如 L7 的热力图需要 blur pass、PBR 中的 SSAO 环境光遮蔽),只能采用其他反走样手段。
## TAA(Temporal Anti-Aliasing) 原理
来自「知乎 - Experimentalize TAA with no code」🔗
> 严格来说 TAA 并不能算一个具体的算法,而是更像一个统一的算法框架。和 SSAA 一样TAA 也能够同时减轻几何走样和着色走样的问题。
关于 TAA 的原理「GDC - Temporal Reprojection
Anti-Aliasing in INSIDE」[🔗](http://twvideo01.ubm-us.net/o1/vault/gdc2016/Presentations/Pedersen_LasseJonFuglsang_TemporalReprojectionAntiAliasing.pdf) 讲的十分清晰。如果相机和物体的相对位置在当前帧之前发生过变化,那么当前帧就可以以若干前序帧进行修正。
![](./screenshots/taa-1.png)
但如果在前序帧中相机和物体都没有发生过变化,那对于当前帧就无从修正了。因此可以对视锥进行抖动,在渲染每一帧之前,使用抖动矩阵对投影矩阵进行偏移,最终实现视锥的偏移:
![](./screenshots/taa-step1.png)
然后在 FS 中,最关键的就是 reproject 这一步:
![](./screenshots/taa-step2.png)
对于静止场景「Three.js - TAA example」[🔗](https://threejs.org/examples/#webgl_postprocessing_taa)、「ECharts.GL - temporalSuperSampling」[🔗](https://echarts.apache.org/zh/option-gl.html#globe.temporalSuperSampling) 都采用了这种方法。
## 实现方法
由于需要对投影矩阵进行抖动,我们需要选取低差异序列。
来自「知乎 - 低差异序列(一)- 常见序列的定义及性质」🔗,右图明显比左图纯随机生成覆盖面广:
![](./screenshots/halton.png)
参考 Echarts.GL我们选择 `Halton(2,3)` 低差异序列:
```typescript
const offset = this.haltonSequence[this.frame % this.haltonSequence.length];
this.cameraService.jitterProjectionMatrix(
((offset[0] * 2.0 - 1.0) / width) * jitterScale,
((offset[1] * 2.0 - 1.0) / height) * jitterScale,
);
```
在每一帧都会尝试进行累加。如果在连续运动过程中TAA 的累加过程必然来不及完成,此时只需要输出当前帧原始结果即可,随后尝试继续轮询累加是否完成。因此在累加完成之前,都会输出当前帧未经 TAA 的结果。
最后我们需要进行加权平均,历史帧的权重应当越来越小:
![](./screenshots/taa-step3.png)
这里我们选择当前帧权重为 0.9,历史帧为 0.1
```typescript
useFramebuffer(this.outputRenderTarget, () => {
this.blendModel.draw({
uniforms: {
u_opacity: layerStyleOptions.opacity || 1,
u_MixRatio: this.frame === 0 ? 1 : 0.9,
u_Diffuse1: this.sampleRenderTarget,
u_Diffuse2:
this.frame === 0
? layer.multiPassRenderer.getPostProcessor().getReadFBO()
: this.prevRenderTarget,
},
});
});
```
最后我们将最终的混合结果“拷贝”给后处理模块,实现渐进增强的效果:
```typescript
useFramebuffer(
layer.multiPassRenderer.getPostProcessor().getReadFBO(),
() => {
this.copyModel.draw({
uniforms: {
u_Texture: this.copyRenderTarget,
},
});
},
);
// 调用后处理模块应用后续效果
layer.multiPassRenderer.getPostProcessor().render(layer);
```
## 最终效果
为了更直观地看到效果,在 DEMO 中我们可以调节相机抖动范围:
![](./screenshots/taa-result.gif)
## 参考资料
* 「知乎 - 反走样技术(一):几何反走样」[🔗](https://zhuanlan.zhihu.com/p/28800047)
* 「知乎 - Experimentalize TAA with no code」[🔗](https://zhuanlan.zhihu.com/p/41642855)
* 「ECharts.GL - temporalSuperSampling」[🔗](https://echarts.apache.org/zh/option-gl.html#globe.temporalSuperSampling)
* 「Mapbox - set custom layers and extrusion examples to use antialias: true」[🔗](https://github.com/mapbox/mapbox-gl-js/pull/8474)
* 「Three.js - TAA example」[🔗](https://threejs.org/examples/#webgl_postprocessing_taa)
* 「Paper - Amortized Supersampling」[🔗](http://hhoppe.com/supersample.pdf)
* 「GDC - Temporal Reprojection Anti-Aliasing in INSIDE」[🔗](http://twvideo01.ubm-us.net/o1/vault/gdc2016/Presentations/Pedersen_LasseJonFuglsang_TemporalReprojectionAntiAliasing.pdf)
* 「知乎 - 低差异序列(一)- 常见序列的定义及性质」[🔗](https://zhuanlan.zhihu.com/p/20197323)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

View File

@ -1,219 +0,0 @@
# 使用方法
L7 提供三种使用方式CDN、Submodule 以及 React 组件。
## 通过 CDN 使用
首先在 `<head>` 中引入 L7 CDN 版本的 JS 文件:
```html
<head>
<script src='https://api.l7/v2.0.0-beta/l7.js'></script>
</head>
```
如果使用 Mapbox还需要额外引入 Mapbox 的 JS 和 CSS 文件,这一步可以参考 [Mapbox 文档](https://docs.mapbox.com/mapbox-gl-js/overview/#quickstart)
```html
<head>
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.css' rel='stylesheet' />
<!-- 上一步引入的 L7 JS 和 CSS -->
</head>
```
⚠️高德采用异步加载,因此不需要引入任何额外静态文件。
然后在 `<body>` 中定义一个容器并设置一个 `id`。通过全局 `L7` 这个命名空间可以获取场景 `L7.Scene` 和图层 `L7.PolygonLayer`
```html
<body>
<div
id="map"
style="position: absolute;top: 0;left: 0;right: 0;bottom: 0;"
></div>
<script>
(async function() {
// 获取数据
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json',
);
const data = await response.json();
// 创建场景
const scene = new L7.Scene({
id: 'map', // 容器 id
map: new L7.Mapbox({ // 高德地图为 L7.AMap
style: 'mapbox://styles/mapbox/streets-v9',
center: [110.19382669582967, 50.258134],
pitch: 0,
zoom: 3,
token: 'pg.xxx', // 高德或者 Mapbox 的 token
}),
});
// 创建图层
const layer = new L7.PolygonLayer({
enablePicking: true,
enableHighlight: true,
passes: [
[
'colorHalftone',
{
size: 8,
},
],
],
});
layer
.source(data)
.size('name', [0, 10000, 50000, 30000, 100000])
.color('name', [
'#2E8AE6',
'#69D1AB',
'#DAF291',
'#FFD591',
'#FF7A45',
'#CF1D49',
])
.shape('fill')
.style({
opacity: 0.8,
});
// 添加图层到场景中
scene.addLayer(layer);
})();
</script>
</body>
```
⚠️需要获取高德或者 Mapbox 的使用 token 并传入 `L7.Scene` 的构造函数,获取方式如下:
* 高德地图开发者 Key [申请方法](https://lbs.amap.com/dev/key/)
* [Mapbox Access Tokens](https://docs.mapbox.com/help/how-mapbox-works/access-tokens/#creating-and-managing-access-tokens)
## 通过 Submodule 使用
首先通过 `npm/yarn` 安装 `@antv/l7@beta`
```bash
npm install --save @antv/l7@beta
// or
yarn add @antv/l7@beta
```
然后就可以使用其中包含的场景和各类图层:
```typescript
import { Scene, PolygonLayer } from '@antv/l7';
import { AMap } from '@antv/l7-maps';
(async function() {
// 获取数据
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json',
);
const data = await response.json();
// 创建场景
const scene = new Scene({
id: 'map',
map: new AMap({
center: [110.19382669582967, 50.258134],
pitch: 0,
style: 'dark',
zoom: 3,
token: 'pg.xxx', // 高德或者 Mapbox 的 token
}),
});
// 创建图层
const layer = new PolygonLayer({});
layer
.source(data)
.size('name', [0, 10000, 50000, 30000, 100000])
.color('name', [
'#2E8AE6',
'#69D1AB',
'#DAF291',
'#FFD591',
'#FF7A45',
'#CF1D49',
])
.shape('fill')
.style({
opacity: 0.8,
});
// 添加图层到场景中
scene.addLayer(layer);
})();
```
L7 目前的文档都通过这种方式使用,可以参考项目中的 stories
* [高德地图](https://github.com/antvis/L7/blob/next/stories/MapAdaptor/components/AMap.tsx)
* [Mapbox](https://github.com/antvis/L7/blob/next/stories/MapAdaptor/components/Mapbox.tsx)
## [WIP] React
React 组件待开发,目前可以暂时以 Submodule 方式使用:
```tsx
import { Scene, PolygonLayer } from '@antv/l7';
import { AMap } from '@antv/l7-maps';
import * as React from 'react';
export default class AMapExample extends React.Component {
private scene: Scene;
public componentWillUnmount() {
this.scene.destroy();
}
public async componentDidMount() {
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/d2e0e930-fd44-4fca-8872-c1037b0fee7b.json',
);
const scene = new Scene({
id: 'map',
map: new AMap({
center: [110.19382669582967, 50.258134],
pitch: 0,
style: 'dark',
zoom: 3,
token: 'pg.xxx', // 高德或者 Mapbox 的 token
}),
});
const layer = new PolygonLayer({});
layer
.source(await response.json())
.size('name', [0, 10000, 50000, 30000, 100000])
.color('name', [
'#2E8AE6',
'#69D1AB',
'#DAF291',
'#FFD591',
'#FF7A45',
'#CF1D49',
])
.shape('fill')
.style({
opacity: 0.8,
});
scene.addLayer(layer);
this.scene = scene;
}
public render() {
return (
<div
id="map"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
);
}
}
```
⚠️组件 Unmount 时需要通过 `scene.destroy()` 手动销毁场景。

View File

@ -1,352 +0,0 @@
# 构建方案
考虑到 L7 提供的三种[使用方法](./使用方法.md)CDN、Submodule 和 React 组件,我们需要提供对应的构建方案。
由于 React 组件待开发,下面我们将从方案技术细节、优化手段两方面介绍 CDN 和 Submodule 的构建方案。
## CDN
考虑到后续将引入 WebWorker 特性,目前 Webpack4 暂时还不支持多种 targetweb + webworker混合的输出模式相关 [ISSUE](https://github.com/webpack/webpack/issues/6525)。
如果后续支持,配合 SplitChunksPlugin 应该能解决在 Worker 和不同 entry 之间共享代码的问题。
因此目前和 Mapbox 做法一样,我们使用 Rollup 构建 CDN Bundler。
打包命令如下,会在 `packages/l7/dist` 下输出产物:
```bash
yarn bundle
```
### UMD
以 L7 为命名空间,让用户可以通过类似 `L7.Scene` 的方式使用。同时以 UMD 为构建目标,输出到 `packages/l7/dist` 下:
```javascript
{
input: resolveFile('build/bundle.ts'),
output: {
file: resolveFile('packages/l7/dist/bundle.js'),
format: 'umd',
name: 'L7',
},
}
```
目前只需要暴露场景以及图层相关的 API因此 Bundler 非常简单:
```typescript
// build/bundle.ts
export * from '@antv/l7';
```
### Alias
为了帮助 resolver 定位 lerna packages需要重命名类似 `@antv/l7-scene` 这样的依赖路径:
```javascript
import alias from '@rollup/plugin-alias';
plugins: [
alias(
{
resolve: ['.tsx', '.ts'],
entries: [
{
find: /^@l7\/(.*)/,
replacement: resolveFile('packages/$1/src'),
},
]
},
),
]
```
配合 [`terser`](https://github.com/TrySound/rollup-plugin-terser) 压缩后,我们就能得到可运行的 CDN 版本了,但从减少构建产物大小出发还有很多优化可以做。
### 减少包大小
除了 Rollup 提供的 TreeShaking我们主要从三个方面考虑
* 减少第三方依赖大小尤其是 Lodash
* external Mapbox 依赖
* 压缩 GLSL 代码
* 去除多余空格、换行符和注释
* 内联 WebGL 常量
* 预计算 define 变量
#### Lodash 按需引用
通过 analysis 插件可以看到第三方依赖大小占比:
```
/node_modules/lodash/lodash.js
███████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 23.79 % (540.328 KB)
/node_modules/regl/dist/regl.js
██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 12.21 % (277.403 KB)
/node_modules/hammerjs/hammer.js
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3.25 % (73.847 KB)
/node_modules/uri-js/dist/es5/uri.all.js
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2.28 % (51.721 KB)
```
仔细查看 Lodash 的引用情况:
```
███████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
file: /node_modules/lodash/lodash.js
bundle space: 23.79 %
rendered size: 540.328 KB
original size: 540.51 KB
code reduction: 0.03 %
dependents: 13
- /packages/core/src/services/layer/StyleAttribute.ts
- /packages/core/src/services/shader/ShaderModuleService.ts
- /packages/core/src/services/renderer/passes/post-processing/BlurHPass.ts
```
按需引用 Lodash 常见的做法有几种:
* [loash-es](https://github.com/lodash/lodash/tree/es)
* babel-plugin-lodash
* lodash-webpack-plugin
由于我们使用 Rollup 以及 `rollup-plugin-babel`[babel-plugin-lodash](https://github.com/lodash/babel-plugin-lodash) 可以很好地解决这个问题。该插件的[原理](https://github.com/rollup/rollup/issues/610#issuecomment-270801483)其实也是引用 `lodash-es`
```javascript
// this...
import { template } from 'lodash-es';
// ...basically becomes this:
import template from 'lodash-es/template.js';
```
最终的效果还是很明显的:
```
/node_modules/regl/dist/regl.js
████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 16.55 % (277.403 KB)
/node_modules/hammerjs/hammer.js
██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4.41 % (73.847 KB)
/node_modules/uri-js/dist/es5/uri.all.js
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3.09 % (51.721 KB)
/node_modules/lodash.mergewith/index.js
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3.06 % (51.256 KB)
```
#### 剔除 Mapbox
不同于高德异步加载的方式Mapbox 用户需要手动引入 Mapbox 的 JS 和 CSS因此 L7 CDN 版本就需要剔除了。通过 `globals` 假定用户负责引入 Mapbox 的 CDN 版本:
```javascript
{
output: {
globals: {
'mapbox-gl': 'mapboxgl',
},
},
external: [
'mapbox-gl',
],
}
```
这样 L7 Bundler 中就不包含 Mapbox 的 Module Bundler(mapbox-gl) 了。
#### 内联 WebGL 常量
在构建阶段可以将 WebGL 常量替换成对应的值,可以减少字符长度:
```javascript
// from
const max = gl.MAX_VERTEX_ATTRIBS;
// to
const max = 34921;
```
luma.gl 和 deck.gl 都使用了 [babel-plugin-inline-webgl-constants](https://www.npmjs.com/package/babel-plugin-inline-webgl-constants)。
来看一下实际效果,在压缩前就能减少字符长度:
```javascript
// 内联前
const usageMap = {
[gl.STATIC_DRAW]: 'static',
[gl.DYNAMIC_DRAW]: 'dynamic',
[gl.STREAM_DRAW]: 'stream'
};
// 内联后
const usageMap = {
[35044]: 'static',
[35048]: 'dynamic',
[35040]: 'stream'
};
```
#### 压缩 GLSL 代码
在开发编写 Shader 时,我们是不需要对 GLSL 代码进行压缩的,因为在 Shader 编译失败时能根据错误信息定位到具体行列。
但是在生产环境下,我们就需要把 GLSL 源代码中包含的**多余**的换行、空格以及注释去掉,减少最终引入字符串的大小。
这里需要注意的是并不是所有换行都可以简单去除,例如 `define` 语句末尾的换行一定要保留。
luma.gl 和 deck.gl 使用了 [babel-plugin-remove-glsl-comments](https://github.com/uber/luma.gl/tree/master/dev-modules/babel-plugin-remove-glsl-comments) 简单地移除注释,但很明显,多余的空格和换行符依然存在。
因此我们需要写一个简单的 Rollup 插件:
```javascript
export default function glsl(include, minify) {
const filter = createFilter(include);
return {
name: 'glsl',
transform(code, id) {
if (!filter(id)) return;
if (minify) {
code = code
.trim() // strip whitespace at the start/end
.replace(/\n+/g, '\n') // collapse multi line breaks
// remove comments
.replace(INLINE_COMMENT_REGEX, '\n')
.replace(BLOCK_COMMENT_REGEX, '')
.replace(/\n\s+/g, '\n') // strip identation
}
return {
code: `export default ${JSON.stringify(code)};`,
map: { mappings: '' }
};
}
};
}
```
#### GLSL minifier
以上针对 GLSL 的压缩仅限于字符替换,更彻底的优化必然需要生成 GLSL 对应的 AST从而进行变量重命名、死代码消除等等更高级的优化手段。[glsl-minifier](https://github.com/TimvanScherpenzeel/glsl-minifier) 就是这样一个 CLI 工具。
其中的预计算特性有点类似 [Prepack](https://github.com/facebook/prepack),在构建阶段就计算出 `define` 变量的值:
```glsl
#define SPREAD 8.00
#define MAX_DIR_LIGHTS 0
#define MAX_POINT_LIGHTS 0
#define MAX_SPOT_LIGHTS 0
#define MAX_HEMI_LIGHTS 0
#define MAX_SHADOWS 0
#define GAMMA_FACTOR 2
uniform mat4 viewMatrix;
uniform vec3 cameraPosition;
uniform vec2 resolution;
uniform float time;
uniform sampler2D texture;
void main() {
vec2 uv = gl_FragCoord.xy / resolution.xy;
float v = texture2D( texture, uv ).x;
if (v == 1000.) discard;
v = sqrt(v);
gl_FragColor = vec4( vec3( 1. - v / SPREAD ), 1.0 );
}
```
上述代码压缩结果如下,`define` 统统不见了,变量名也进行了改写:
```glsl
uniform highp vec2 resolution;uniform sampler2D texture;void main(){highp vec2 a;a=(gl_FragCoord.xy/resolution);lowp vec4 b;b=texture2D(texture,a);if((b.x==1000.0)){discard;}lowp vec4 c;c.w=1.0;c.xyz=vec3((1.0-(sqrt(b.x)/8.0)));gl_FragColor=c;}
```
当然 glsl-minifier 做的远不止这些,还会应用变量名改写、死代码消除等等优化手段:
> Optimisations include function inlining, dead code removal, copy propagation, constant folding, constant propagation, arithmetic optimizations and so on. Minifications includes variable rewriting and whitespace trimming.
显然这种手段要求我们的 Shader 代码在构建时是稳定的,然而 L7 使用的 GLSL 模块化方案需要在运行时进行模块拼接,如果在构建时代码片段中包含的变量发生了改写,势必影响运行时的拼接结果。另外 minifier 会校验代码的正确性,不理解我们自定义的模块引入语句 `pragma include 'module'` 是一定会报错的。
以这样的 Shader 为例:
```glsl
#pragma include "project"
void main() {
// 从 project 模块引入方法
project(position);
}
```
执行压缩时会报错:
```bash
$ node_modules/.bin/glsl-minifier -i ./build/example.frag -o ./build/example.min.frag
Error:
(28,2): error: no function with name 'project'
Exiting glsl-minifier!
```
因此要想使用这个终极压缩方案,需要修改 L7 目前的 GLSL 模块化方案,代码拼接不能在运行时而需要在构建时完成。但这样就很难兼顾扩展性,毕竟用户自定义图层的 Shader 代码肯定只有运行时才能拿到。
所以一个折中的办法是在构建时先对 L7 内置图层的 Shader 代码进行模块化处理,得到最终的 GLSL 文本,然后再 minify。同时保留运行时模块化拼接的能力应对用户自定义图层。
## Submodule
npm 和 yarn 只提供了例如 `npm link` 以及 `yarn link` 这样的功能,而 yarn workspaces 只提供了 monorep 需要的底层 link 功能。相比之下 lerna 提供了更高级的功能例如 publish 和 version。因此 yarn workspaces 和 lerna 完全可以组合使用,这也是例如 Jest 等大型项目的使用方式。
![](./screenshots/monorep.png)
构建命令如下,会在各个 package 下生成 `/lib``/es` 两个文件夹分别包含 ES2015 和 ESModule 产物:
```bash
yarn build
```
### 编译 TS
使用 TS 有两种构建方式:
* native TypeScript with tsc
* [@babel/preset-typescript](https://babeljs.io/docs/en/babel-preset-typescript)
由于我们的项目中需要使用到一些 babel plugin装饰器、引入 GLSL 等),因此后者显然是更好的选择。这里我们使用 babel 7 的项目全局配置 configFile。
为了合并 ES2015 与 ESModule我们参考 [redux](https://babeljs.io/blog/2018/06/26/on-consuming-and-publishing-es2015+-packages#conflating-javascript-modules-and-es2015)
```json
// redux package.json
{
"main": "lib/redux.js", // ES5 + Common JS
"module": "es/redux.js", // ES5 + JS Modules
}
```
开发模式加上 `--watch` 即可。
* `--root-mode upward` 使用 root 下的 babel 配置文件
* `--out-dir dist` 输出到 /dist 文件夹下
* `--delete-dir-on-start` 每次构建前清空,因此不需要 `rimraf`
```json
"scripts": {
"build": "father build",
"build:cjs": "BABEL_ENV=cjs babel src --root-mode upward --out-dir lib --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments",
"build:esm": "BABEL_ENV=esm babel src --root-mode upward --out-dir es --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments"
},
```
### 生成 TS 声明文件
和构建前类型检查不同,此时我们需要 tsc 输出类型声明文件了:
```json
{
"postbuild": "yarn build:declarations",
"build:declarations": "lerna exec --stream --no-bail 'tsc --project ./tsconfig.build.json'"
}
```
当然不需要包含 story 和测试用例:
```json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"emitDeclarationOnly": true,
"declaration": true,
"rootDir": "./",
"baseUrl": "./",
"paths": {
"@antv/l7-*": ["packages/*/src"],
"@antv/l7": ["packages/l7/src"],
"*": ["node_modules", "packages"]
}
},
"exclude": ["**/*.story.*", "**/__tests__/**/*", "**/*.spec.*", "dist"],
"include": []
}
```
### 按需引入地图依赖
以 L7 Bundler 方式使用时,由于需要在运行时根据用户配置项选择地图底图,会导致构建时需要将全部地图依赖引入,无法进行 TreeShaking。
目前高德地图使用运行时异步加载方式引入,不会导致该问题,但 Mapbox 同样使用 Bundler对于高德用户就多余了。
[ISSUE](https://github.com/antvis/L7/issues/86)

View File

@ -1,142 +0,0 @@
# 自动化测试方案
如何测试一个 WebGL 应用渲染结果是否正确呢?常用的做法是进行像素比对,当然这也只能用于一些简单的判断例如渲染是否成功,整体的 Snapshot 比对开销很大。
但不管怎么说,这都意味着我们必须使用 WebGL API 进行真实的渲染。
以上过程在测试用例中描述如下:
```javascript
// 1. 绘制
// 2. 读取像素
const pixels = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
// 3. 判断某个像素点是否符合预期
```
在与测试框架结合时,常用的方案有:
* Electron 除了 WebGL APIDOM API 等其他浏览器实现对于 WebGL 测试都是多余的,在 CI 时需要安装的依赖过大,所需的启动时间也很长。
* [node-webgl](https://github.com/mikeseven/node-webgl) 不同于 WebGL可以直接调用 OpenGL 驱动,但同样包含了很多 WebGL 之外的特性。
这里我们选择 [headless-gl](https://github.com/stackgl/headless-gl),一个纯粹的 WebGL 1 规范的实现。并且能够很容易集成进现有的 [CI 流程](https://github.com/stackgl/headless-gl#how-can-i-use-headless-gl-with-a-continuous-integration-service)中,例如 [TravisCI](https://travis-ci.org/) 和 [AppVeyor](http://www.appveyor.com/)。
## 测试框架
在配置测试框架前,我们必须解决一个 WebGL 项目中常见的问题。
### 引入 GLSL 文件
如何在测试时正确引入 GLSL 文件是一个问题。目前各个 3D 引擎常用的做法有两种:
* 以字符串形式直接写在 `.js` 文件中。`luma.gl/deck.gl` 使用[这种方式](https://github.com/uber/deck.gl/blob/7.1-release/modules/layers/src/arc-layer/arc-layer-fragment.glsl.js)。
* 使用 `.glsl` 编写,测试前使用构建脚本自动生成对应的 `.js` 文件。`Three.js`、`clay.gl` 使用[这种方式](https://github.com/pissang/claygl/blob/master/build/glsl2js.js)。
前者的好处是测试流程无需做过多修改,坏处则是无法享受编辑器对于 GLSL 的语法高亮,影响开发体验。而后者又需要编写额外的 `glsl2js` 的转换脚本。
我们显然希望如后者一样直接写 GLSL但最好能让测试框架帮助我们完成自动转换的工作。
之前我们选择 `@babel/preset-typescript` 而非官方 `tsc` 的一大原因就是可以使用 `babel` 丰富的插件,`babel-plugin-inline-import` 就能完成类似 webpack 中 `raw-loader` 的功能,直接以字符串形式引入 GLSL 代码:
```javascript
// 以字符串形式引入 GLSL 代码
import circleFrag from '../../shaders/circle_frag.glsl';
```
这样测试框架只需要使用同一套 babel 项目全局配置就行了。我们使用 Jest
```javascript
// jest.config.js
module.exports = {
transform: {
'^.+\\.(ts|tsx)$': 'babel-jest',
},
}
```
下面就可以编写测试用例了。
## 测试用例编写
我们将测试用例分成三类:
* 内部服务的单元测试
* 渲染服务结果的 Snapshot 快照测试
* React 组件测试
将测试用例放在任意路径的 `__tests__` 文件夹下并以 `xxx.spec.ts` 命名就可以帮助框架发现并执行了。
使用 `yarn test` 运行所有测试用例:
![](./screenshots/jest.png)
### 单元测试
这类测试直接使用 Jest API 就好了,我们以 `@antv/l7-core` 模块的 `ShaderModuleService` 为例,编写一个简单的测试用例:
```typescript
// services/shader/__test__/shader.spec.ts
import 'reflect-metadata';
import IShaderModuleService from '../IShaderModuleService';
import ShaderModuleService from '../ShaderModuleService';
describe('ShaderService', () => {
let shaderService: IShaderModuleService;
beforeEach(() => {
shaderService = new ShaderModuleService();
});
it('should register common module correctly and generate fragment/vertex shader code', () => {
const rawShaderCode = `
#define PI 3.14
`;
const commonModule = {
fs: rawShaderCode,
vs: rawShaderCode,
};
shaderService.registerModule('common', commonModule);
const { vs, fs } = shaderService.getModule('common');
expect(vs).toMatch(/3\.14/);
expect(fs).toMatch(/3\.14/);
});
});
```
### 渲染结果测试
得益于 L7 使用的基于 Inversify 的依赖注入方案,我们能够很轻易地将渲染服务替换为基于 headless-gl 的渲染服务。
具体到我们目前的渲染服务实现 `regl`,它能轻易做到这一点。事实上 regl 的[测试用例](https://github.com/regl-project/regl/blob/gh-pages/test/util/create-context.js#L28)也是这样使用的。
### [WIP] React 组件测试
### Coverage Report
我们使用 Coveralls.io
```json
// package.json
"coveralls": "jest --coverage && cat ./tests/coverage/lcov.info | coveralls",
```
运行 `yarn coveralls` 可以查看代码覆盖率,我们为分支、行覆盖率等指标设置了阈值:
```javascript
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
```
## TravisCI
TravisCI 检测到 `yarn.lock` 就会默认安装 `yarn` 并使用它安装依赖,所以不需要[额外的配置](https://yarnpkg.com/en/docs/install-ci#travis-tab)。
TravisCI 配合之前的 Coveralls.io。

View File

@ -1,174 +0,0 @@
# 自定义后处理效果
L7 自定义了一套较为灵活的渲染管线,在后处理效果方面提供了最大程度的扩展性。除了直接使用内置的常用效果(例如下图中 ColorHalftone、六边形像素化、噪声、Sepia开发者还可以自定义任何后处理效果在场景中完成注册即可应用到任意图层上。
![](./screenshots/multi-scene.png)
下面我们以 [glfx.js](http://evanw.github.io/glfx.js/demo/) 中的 Dot Screen 效果(下图)为例,介绍如何在 PolygonLayer 中应用这种效果。完整 DEMO 代码[在此]()。
![](./screenshots/custom-effect.png)
我们将分成三步介绍:
1. 定义效果
* 定义效果参数
* 继承后处理效果基类
* 编写 Fragment Shader
2. 在场景中注册效果
3. 在图层中使用效果
## 定义效果
### 定义效果参数
实现任何一种后处理效果,我们都希望提供一些灵活的参数,可以在运行时供使用者修改。以我们需要实现的 Dot Screen 效果为例,参数接口 `IDotScreenEffectConfig` 定义如下:
```typescript
interface IDotScreenEffectConfig {
center: [number, number]; // pattern 圆心
angle: number; // dot 旋转角度
size: number; // dot 尺寸
}
```
### 继承后处理效果基类
为了最大程度减少样板代码L7 提供了 `BasePostProcessingPass` 基类供子类继承,同时通过泛型将上一步定义的参数接口传入:
```typescript
import { BasePostProcessingPass } from '@antv/l7';
class DotScreenEffect extends BasePostProcessingPass<IDotScreenEffectConfig> {
//... 省略重载方法
}
```
接下来我们只需要重载基类的一个方法 `setupShaders`。在这个方法中我们可以使用 L7 内置的服务,例如 Shader 服务、渲染服务等,各服务说明及 API 使用方式[详见](./IoC%20容器、依赖注入与服务说明.md)。
```typescript
protected setupShaders() {
// 使用 Shader 服务注册 GLSL 模块
this.shaderModuleService.registerModule('dotScreenEffect', {
vs: this.quad, // Vertex Shader 固定
fs: ``, // 暂时省略,在下一小节中详细介绍
});
// 使用 Shader 服务获取编译后的 GLSL 模块
const { vs, fs, uniforms } = this.shaderModuleService.getModule('dotScreenEffect');
// 使用渲染器服务获取视口尺寸
const { width, height } = this.rendererService.getViewportSize();
return {
vs,
fs,
uniforms: {
...uniforms,
u_ViewportSize: [width, height],
},
};
}
```
### 编写 Fragment Shader
在编写 Fragment Shader 时,可以按照如下模版。由于 L7 实现了简单的 GLSL 模块化,可以使用一些特殊的语法,例如:
* Uniform 设置默认值
* 引入 L7 内置 GLSL 模块
```glsl
varying vec2 v_UV;
uniform sampler2D u_Texture;
uniform vec2 u_ViewportSize : [1.0, 1.0];
// 自定义效果参数声明
// 自定义效果函数 myCustomEffect 定义
void main() {
// 纹理采样
gl_FragColor = vec4(texture2D(u_Texture, v_UV));
// 应用自定义效果函数
gl_FragColor = myCustomEffect(gl_FragColor, u_ViewportSize, v_UV);
}
```
Dot Screen 效果具体 GLSL 代码可以参考 [luma.gl](https://github.com/uber/luma.gl/blob/master/modules/engine/src/effects/shader-modules/fun-filters/dotscreen.js#L11-L30)。
完整 Fragment Shader 代码如下。需要注意的是这里 Uniform 名需要和效果配置项属性名统一,即 `size` 对应 `u_Size`、`angle` 对应 `u_Angle`。:
```glsl
varying vec2 v_UV;
uniform sampler2D u_Texture;
uniform vec2 u_ViewportSize : [1.0, 1.0];
// 自定义效果参数及默认值声明
uniform vec2 u_Center : [0.5, 0.5];
uniform float u_Angle : 1;
uniform float u_Size : 3;
// 自定义效果实现
// @see https://github.com/uber/luma.gl/blob/master/modules/engine/src/effects/shader-modules/fun-filters/dotscreen.js#L11-L30
float pattern(vec2 texSize, vec2 texCoord) {
float scale = 3.1415 / u_Size;
float s = sin(u_Angle), c = cos(u_Angle);
vec2 tex = texCoord * texSize - u_Center * texSize;
vec2 point = vec2(
c * tex.x - s * tex.y,
s * tex.x + c * tex.y
) * scale;
return (sin(point.x) * sin(point.y)) * 4.0;
}
vec4 dotScreen_filterColor(vec4 color, vec2 texSize, vec2 texCoord) {
float average = (color.r + color.g + color.b) / 3.0;
return vec4(vec3(average * 10.0 - 5.0 + pattern(texSize, texCoord)), color.a);
}
void main() {
gl_FragColor = vec4(texture2D(u_Texture, v_UV));
gl_FragColor = dotScreen_filterColor(gl_FragColor, u_ViewportSize, v_UV);
}
```
至此我们就完成了效果的定义。
## 在场景中注册
一种效果要想生效,必须先在场景中完成注册。`scene.registerPostProcessingPass()` 接受两个参数分别为上一步定义的效果构造函数以及效果名。
后续我们在图层中使用时就可以通过效果名引用了:
```typescript
// 场景定义
const scene = new Scene({
id: 'map',
map: new Mapbox({
style: 'mapbox://styles/mapbox/streets-v9',
center: [110.19382669582967, 50.258134],
pitch: 0,
zoom: 3,
}),
});
// 注册自定义后处理效果
scene.registerPostProcessingPass(
DotScreenEffect, // 效果构造函数
'dotScreenEffect', // 效果名,便于后续在图层中引用
);
```
## 在图层中使用效果
和 L7 内置的后处理效果使用方法一致,通过效果名引用,同时传入定义参数即可:
```typescript
const layer = new PolygonLayer({
enablePicking: true,
enableHighlight: true,
passes: [
[
'dotScreenEffect', // 引用效果名
{
size: 8, // 传入参数
angle: 1,
},
],
],
});
```
最终效果如下:
![](./screenshots/dotscreen.png)

View File

@ -6,12 +6,6 @@
"url": "https://github.com/antvis/L7"
},
"devDependencies": {
"@antv/g2": "^4.2.8",
"@antv/l7-district": "^2.3.9",
"@antv/l7-draw": "^3.0.9",
"@antv/l7-react": "^2.3.3",
"@antv/l7plot": "^0.1.0",
"@antv/larkmap": "^0.10.0",
"@babel/cli": "^7.6.4",
"@babel/core": "^7.18.10",
"@babel/eslint-parser": "^7.18.9",
@ -87,8 +81,6 @@
"eslint-plugin-html": "^6.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"father": "^4.1.0",
"gcoord": "^0.3.2",
"geotiff": "1.0.0-beta.10",
"gh-pages": "^2.1.1",
"gl": "^5.0.3",
"glsl-minifier": "^0.0.13",
@ -153,10 +145,7 @@
"webpack-dev-server": "^3.1.7",
"webpack-merge": "^4.1.4",
"wellknown": "^0.5.0",
"worker-loader": "^2.0.0",
"yorkie": "^2.0.0",
"crypto-js": "^4.1.1",
"jsencrypt": "^3.2.1"
"worker-loader": "^2.0.0"
},
"scripts": {
"dev": "npm run worker && dumi dev",

View File

@ -1,31 +0,0 @@
export default {
// more father 4 config: https://github.com/umijs/father-next/blob/master/docs/config.md
esm: {
output:'es'
},
cjs: {
output:'lib'
},
platform:'browser',
autoprefixer: {
browsers: ['IE 11', 'last 2 versions'],
},
extraBabelPresets: [
'@babel/preset-typescript'
],
extraBabelPlugins: [
// 开发模式下以原始文本引入,便于调试
[
// import glsl as raw text
'babel-plugin-inline-import',
{
extensions: [
'.glsl'
]
}
],
[
'transform-import-css-l7'
],
],
};

View File

@ -1,3 +0,0 @@
lib
es
dist

View File

@ -1,325 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.1.12](https://github.com/antvis/L7/compare/v2.1.11...v2.1.12) (2020-04-10)
**Note:** Version bump only for package @antv/l7-component
## [2.1.11](https://github.com/antvis/L7/compare/v2.1.10...v2.1.11) (2020-04-07)
**Note:** Version bump only for package @antv/l7-component
## [2.1.8](https://github.com/antvis/L7/compare/v2.1.7...v2.1.8) (2020-03-26)
**Note:** Version bump only for package @antv/l7-component
## [2.1.7](https://github.com/antvis/L7/compare/v2.1.6...v2.1.7) (2020-03-26)
**Note:** Version bump only for package @antv/l7-component
## [2.1.5](https://github.com/antvis/L7/compare/v2.1.4...v2.1.5) (2020-03-20)
**Note:** Version bump only for package @antv/l7-component
## [2.1.3](https://github.com/antvis/L7/compare/v2.0.36...v2.1.3) (2020-03-17)
### Bug Fixes
* mapbox 光照问题 ([20d2a6d](https://github.com/antvis/L7/commit/20d2a6d8b803ca3ad87cc1ef69a59d1e3d348cef))
## [2.1.2](https://github.com/antvis/L7/compare/v2.0.36...v2.1.2) (2020-03-15)
### Bug Fixes
* mapbox 光照问题 ([20d2a6d](https://github.com/antvis/L7/commit/20d2a6d8b803ca3ad87cc1ef69a59d1e3d348cef))
## [2.1.1](https://github.com/antvis/L7/compare/v2.0.36...v2.1.1) (2020-03-15)
### Bug Fixes
* mapbox 光照问题 ([20d2a6d](https://github.com/antvis/L7/commit/20d2a6d8b803ca3ad87cc1ef69a59d1e3d348cef))
## [2.0.34](https://github.com/antvis/L7/compare/v2.0.32...v2.0.34) (2020-03-02)
**Note:** Version bump only for package @antv/l7-component
# [2.0.0-beta.28](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.28) (2020-01-02)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-beta.27](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.27) (2020-01-01)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-alpha.28](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-alpha.28) (2020-01-01)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-alpha.27](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-alpha.27) (2019-12-31)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-beta.26](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.26) (2019-12-30)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-beta.25](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.25) (2019-12-27)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-beta.24](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.24) (2019-12-23)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-beta.23](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.23) (2019-12-23)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-beta.21](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.21) (2019-12-18)
### Bug Fixes
* merge master fix conflict ([652e5d1](https://github.com/antvis/L7/commit/652e5d1cafc350fe98d569f32bf6c592c6a79b89))
### Features
* **layer:** add setSelect setActive 方法 & refactor color util ([5c27d66](https://github.com/antvis/L7/commit/5c27d66a6401192f5e0406a2f4c3e0983dc2867c))
# [2.0.0-beta.20](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.20) (2019-12-12)
**Note:** Version bump only for package @antv/l7-component
# [2.0.0-beta.19](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.19) (2019-12-08)
**Note:** Version bump only for package @antv/l7-component
# [2.0.0-beta.18](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.18) (2019-12-08)
**Note:** Version bump only for package @antv/l7-component
# [2.0.0-beta.17](https://github.com/antvis/L7/compare/v2.0.0-beta.16...v2.0.0-beta.17) (2019-12-08)
**Note:** Version bump only for package @antv/l7-component
# [2.0.0-beta.16](https://github.com/antvis/L7/compare/v2.0.0-beta.15...v2.0.0-beta.16) (2019-11-29)
**Note:** Version bump only for package @antv/l7-component
# [2.0.0-beta.15](https://github.com/antvis/L7/compare/v2.0.0-beta.14...v2.0.0-beta.15) (2019-11-29)
### Bug Fixes
* **control:** lint error ([c863d7c](https://github.com/antvis/L7/commit/c863d7c8d15e560e3dfaf39d0ea3fac3242d776a))
* **map:** temporarily closed amap offset coordinate ([9a20f64](https://github.com/antvis/L7/commit/9a20f6480321c9297ff27fe4cfe6af9032fcb969))
# [2.0.0-beta.13](https://github.com/antvis/L7/compare/v2.0.0-beta.12...v2.0.0-beta.13) (2019-11-28)
**Note:** Version bump only for package @antv/l7-component
# [2.0.0-beta.12](https://github.com/antvis/L7/compare/v2.0.0-beta.11...v2.0.0-beta.12) (2019-11-28)
### Bug Fixes
* **component:** fix marker ([14d4818](https://github.com/antvis/L7/commit/14d48184a1579241b077110ed51a8358de25e010))
# 2.0.0-beta.11 (2019-11-28)
### Bug Fixes
* **doc:** file name lowercase ([3cbdc9c](https://github.com/antvis/L7/commit/3cbdc9c7f1d9be34e9c917f05531323946993eb4))
* **fix confilict:** conflict ([8a09ae2](https://github.com/antvis/L7/commit/8a09ae24bef7ba845e5b16759b3ecac210e472c5))
* **fix css:** fix css png ([f7e5376](https://github.com/antvis/L7/commit/f7e5376b7d6c64b2b078dca8f2a230f4fce14c68))
* **fix css:** fix css png ([da604e2](https://github.com/antvis/L7/commit/da604e266f36b70fcc7faa23fa7fe3359d3a1318))
* **layerservice:** fix init bugs in layer service ([8cbbf7b](https://github.com/antvis/L7/commit/8cbbf7b28d63f4df16f061a4ae21726f243e7108))
* **layerservice:** fix init bugs in layer service ([8844243](https://github.com/antvis/L7/commit/8844243050f619b28043c4e9ed1942fe172f561e))
* **merge:** fix conflict ([07e8505](https://github.com/antvis/L7/commit/07e85059ebd40506623253feb624ee3083f393ae))
* **merge branch:** fix confilt ([e7a46a6](https://github.com/antvis/L7/commit/e7a46a691d9e67a03d733fd565c6b152ee8715b6))
* **rm cache:** rm cache ([51ea07e](https://github.com/antvis/L7/commit/51ea07ea664229f775b7c191cfde68299cc8c2d5))
* **site:** megre conflict ([1b5619b](https://github.com/antvis/L7/commit/1b5619b3945e97919e0c616a48ba2265a2a95c22))
### Features
* **chart:** add chart demo ([2a19b07](https://github.com/antvis/L7/commit/2a19b07c1bca7dfbf191618f15ab06a18c262148))
* **component:** add layer control ([7f4646e](https://github.com/antvis/L7/commit/7f4646efd3b0004fde4e9f6860e618c7668af1a7))
* **component:** add scale ,zoom, popup, marker map method ([a6baef4](https://github.com/antvis/L7/commit/a6baef4954c11d9c6582c27de2ba667f18538460))
* **demo:** add point chart demo ([8c2e4a8](https://github.com/antvis/L7/commit/8c2e4a82bf7a49b29004d5e261d8e9c46cd0bd9d))
* **map:** adjust Scene API, use @antv/l7-maps instead ([77b8f21](https://github.com/antvis/L7/commit/77b8f21b0bcf8b06e88d8e0bef213935bf32b957)), closes [#86](https://github.com/antvis/L7/issues/86)

View File

@ -1,54 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import ButtonControl from '../src/control/baseControl/buttonControl';
import { createL7Icon } from '../src/utils/icon';
class TestControl extends ButtonControl {}
describe('buttonControl', () => {
const scene = TestScene();
it('life cycle', () => {
const control = new TestControl();
scene.addControl(control);
const container = control.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
scene.removeControl(control);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
it('disable', () => {
const control = new TestControl();
scene.addControl(control);
control.setIsDisable(true);
expect(control.getContainer().getAttribute('disabled')).not.toBeNull();
control.setIsDisable(false);
expect(control.getContainer().getAttribute('disabled')).toBeNull();
});
it('options', () => {
const control = new TestControl({
title: '导出图片',
btnText: '导出图片',
btnIcon: createL7Icon('l7-icon-tupian'),
});
scene.addControl(control);
const container = control.getContainer();
expect(container.classList).toContain('l7-button-control');
expect(container.getAttribute('title')).toContain('导出图片');
const textContainer = container.querySelector('.l7-button-control__text')!;
expect(textContainer).toBeInstanceOf(HTMLElement);
control.setOptions({
title: undefined,
btnText: '替换文本',
btnIcon: createL7Icon('l7-icon-tupian1'),
});
expect(container.getAttribute('title')).toBeFalsy();
});
});

View File

@ -1,79 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import { DOM } from '@antv/l7-utils';
import { Control } from '../src/control/baseControl';
import { Zoom } from '../src/control/zoom';
class TestControl extends Control {
public onAdd(): HTMLElement {
return DOM.create('div');
}
public onRemove(): void {}
}
describe('control', () => {
const scene = TestScene();
it('life cycle', () => {
const className1 = 'testControl1';
const className2 = 'testControl2';
const control1 = new TestControl({
className: className1,
});
const control2 = new TestControl({
className: className2,
});
scene.addControl(control1);
scene.addControl(control2);
const dom1 = document.querySelector(`.${className1}`);
expect(dom1).toBeInstanceOf(HTMLElement);
const dom2 = document.querySelector(`.${className2}`);
expect(dom2).toBeInstanceOf(HTMLElement);
scene.removeControl(control1);
scene.removeControl(control2);
const dom3 = document.querySelector(`.${className1}`);
expect(dom3).toBeNull();
const dom4 = document.querySelector(`.${className2}`);
expect(dom4).toBeNull();
});
it('show hide', () => {
const control = new TestControl();
scene.addControl(control);
control.hide();
expect(control.getContainer().classList).toContain('l7-control--hide');
expect(control.getIsShow()).toEqual(false);
control.show();
expect(control.getContainer().classList).not.toContain('l7-control--hide');
expect(control.getIsShow()).toEqual(true);
});
it('options', () => {
const className = 'gunala';
const color = 'rgb(255, 0, 0)';
const control = new TestControl({});
scene.addControl(control);
control.setOptions({
position: 'leftbottom',
className,
style: `color: ${color};`,
});
const container = control.getContainer();
const corner = container.parentElement!;
expect(corner.classList).toContain('l7-left');
expect(corner.classList).toContain('l7-bottom');
expect(container.classList).toContain(className);
expect(container.style.color).toEqual(color);
});
// 测试自定义位置
it('position', () => {
const dom = document.createElement('div');
const zoom = new Zoom({
position: dom,
});
scene.addControl(zoom);
expect(dom.firstChild).toEqual(zoom.getContainer());
});
});

View File

@ -1,32 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import ExportImage from '../src/control/exportImage';
describe('exportImage', () => {
const scene = TestScene();
it('life cycle', () => {
const control = new ExportImage({});
scene.addControl(control);
const container = control.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
scene.removeControl(control);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
it('image', () => {
const control = new ExportImage({
onExport: (base64) => {
// tslint:disable-next-line:no-console
// console.log(base64);
},
});
scene.addControl(control);
const button = control.getContainer() as HTMLDivElement;
button.click();
expect(button.parentElement).toBeInstanceOf(HTMLElement);
});
});

View File

@ -1,27 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import Fullscreen from '../src/control/fullscreen';
describe('fullscreen', () => {
const scene = TestScene();
it('life cycle', () => {
const control = new Fullscreen({});
scene.addControl(control);
const container = control.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
scene.removeControl(control);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
it('fullscreen', () => {
const control = new Fullscreen({});
scene.addControl(control);
const button = control.getContainer() as HTMLDivElement;
button.click();
expect(button.parentElement).toBeInstanceOf(HTMLElement);
});
});

View File

@ -1,43 +0,0 @@
import { PointLayer } from '@antv/l7-layers';
import { TestScene } from '@antv/l7-test-utils';
import LayerPopup from '../src/popup/layerPopup';
describe('popup', () => {
const scene = TestScene();
const testClassName = 'l7-layer-popup-test';
it('life cycle', () => {
const pointLayer = new PointLayer();
pointLayer.source([{ lng: 120, lat: 30 }], {
parser: {
type: 'json',
x: 'lng',
y: 'lat',
},
});
const layerPopup = new LayerPopup({
className: testClassName,
items: [
{
layer: pointLayer,
fields: [
{
field: 'lng',
},
],
},
],
});
scene.addPopup(layerPopup);
expect(layerPopup.isOpen()).toEqual(true);
layerPopup.setOptions({
trigger: 'click',
});
scene.removePopup(layerPopup);
expect(layerPopup.isOpen()).toEqual(false);
});
});

View File

@ -1,19 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import LayerSwitch from '../src/control/layerSwitch';
describe('layerSwitch', () => {
const scene = TestScene();
it('life cycle', () => {
const layerSwitch = new LayerSwitch();
scene.addControl(layerSwitch);
const container = layerSwitch.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
expect(layerSwitch.getLayerVisible()).toEqual([]);
scene.removeControl(layerSwitch);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
});

View File

@ -1,41 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import MapTheme from '../src/control/mapTheme';
describe('mapTheme', () => {
const scene = TestScene();
it('life cycle', () => {
const control = new MapTheme({});
scene.addControl(control);
const container = control.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
scene.removeControl(control);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
it('mapTheme', () => {
const control = new MapTheme({
defaultValue: 'normal',
});
scene.addControl(control);
const options = control.getOptions().options;
expect(options.length).toBeGreaterThan(0);
expect(control.getSelectValue()).toEqual(
'mapbox://styles/mapbox/streets-v11',
);
const optionList = ((control
.getPopper()
.getContent() as HTMLDivElement).querySelectorAll(
'.l7-select-control-item',
) as unknown) as HTMLDivElement[];
optionList[1].click();
// expect(control.getSelectValue()).toEqual(
// 'mapbox://styles/zcxduo/ck2ypyb1r3q9o1co1766dex29',
// );
});
});

View File

@ -1,60 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import Marker from '../src/marker';
import Popup from '../src/popup/popup';
const popup = new Popup({ offsets: [0, 20] }).setHTML(
'<h1 onclick= alert("123")>111</h1>',
);
const marker = new Marker().setLnglat({ lng: 120, lat: 30 }).setPopup(popup);
TestScene().addMarker(marker);
describe('Marker', () => {
it('render and remove correctly', () => {
expect(document.querySelector('.l7-marker')).toBeTruthy();
expect(marker.getDefault().draggable).toEqual(false);
expect(marker.getDefault().color).toEqual('#5B8FF9');
expect(marker.getOffset()).toEqual([0, 0]);
expect(marker.isDraggable()).toEqual(false);
marker.remove();
expect(document.querySelector('.l7-marker')).toBeFalsy();
});
it('open popup and close popup correctly', () => {
marker.openPopup();
expect(marker.getPopup().isOpen()).toBeTruthy();
marker.getPopup().close();
expect(marker.getPopup().isOpen()).toBeFalsy();
marker.togglePopup();
expect(marker.getPopup().isOpen()).toBeTruthy();
marker.closePopup();
expect(marker.getPopup().isOpen()).toBeFalsy();
});
it('longitude and latitude', () => {
const { lng, lat } = marker.getLnglat();
expect(lng).toEqual(120);
expect(lat).toEqual(30);
marker.setLnglat({ lng: 121, lat: 31 });
const { lng: newLng, lat: newLat } = marker.getLnglat();
expect(newLng).toEqual(121);
expect(newLat).toEqual(31);
});
it('element', () => {
const el = document.createElement('div');
el.id = 'markerDom';
el.innerHTML = '<h1>111</h1>';
marker.setElement(el);
expect(marker.getElement()).toBeTruthy();
});
it('extData', () => {
marker.setExtData({ test: 1 });
expect(marker.getExtData()).toEqual({ test: 1 });
});
});

View File

@ -1,31 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import MouseLocation from '../src/control/mouseLocation';
describe('buttonControl', () => {
const scene = TestScene();
it('life cycle', () => {
const control = new MouseLocation({});
scene.addControl(control);
const container = control.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
scene.removeControl(control);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
it('life cycle', () => {
const control = new MouseLocation();
scene.addControl(control);
(scene.getMapService().map as any).emit('mousemove', {
lngLat: {
lng: 120,
lat: 30,
},
});
expect(control.getLocation()).toEqual([120, 30]);
});
});

View File

@ -1,16 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import Navigation from '../src/control/geoLocate';
describe('navigation', () => {
const scene = TestScene();
it('navigation', () => {
const control = new Navigation({});
scene.addControl(control);
const button = control.getContainer() as HTMLDivElement;
button.click();
expect(button.parentElement).toBeInstanceOf(HTMLElement);
});
});

View File

@ -1,38 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import PopperControl from '../src/control/baseControl/popperControl';
class TestControl extends PopperControl {}
describe('popperControl', () => {
const scene = TestScene();
it('life cycle', () => {
const control = new TestControl({});
scene.addControl(control);
const container = control.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
scene.removeControl(control);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
it('popper', () => {
const control = new TestControl({
popperTrigger: 'click',
});
scene.addControl(control);
});
it('options', () => {
const control = new TestControl({});
scene.addControl(control);
const testClassName = 'testPopper';
control.setOptions({
popperClassName: testClassName,
});
expect(control.getPopper().getPopperDOM().classList).toContain(
testClassName,
);
});
});

View File

@ -1,43 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import Popup from '../src/popup/popup';
describe('popup', () => {
const scene = TestScene();
const className = 'text-class-popup';
it('life cycle', () => {
const popup = new Popup({
html: '123456',
className: className,
lngLat: {
lng: 120,
lat: 30,
},
});
popup.setOptions({
lngLat: { lng: 130, lat: 40 },
});
scene.addPopup(popup);
const targetPopup = document.querySelector(`.${className}`) as HTMLElement;
expect(targetPopup).not.toBeFalsy();
expect(popup.getLnglat()).toEqual({
lng: 130,
lat: 40,
});
expect(/123456/.test(targetPopup.innerHTML)).toEqual(true);
expect(targetPopup.classList.contains('l7-popup-hide')).toEqual(false);
popup.hide();
expect(targetPopup.classList.contains('l7-popup-hide')).toEqual(true);
popup.show();
expect(targetPopup.classList.contains('l7-popup-hide')).toEqual(false);
});
});

View File

@ -1,38 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import Scale from '../src/control/scale';
describe('scale', () => {
const scene = TestScene();
it('life cycle', () => {
const scale = new Scale();
scene.addControl(scale);
const container = scale.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
expect(
/\d+\s?km/i.test(
container
.querySelector('.l7-control-scale-line')
?.innerHTML.toLowerCase() ?? '',
),
).toEqual(true);
scale.setOptions({
metric: false,
imperial: true,
});
expect(
/\d+\s?mi/i.test(
container
.querySelector('.l7-control-scale-line')
?.innerHTML.toLowerCase() ?? '',
),
).toEqual(true);
scene.removeControl(scale);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
});

View File

@ -1,105 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import SelectControl from '../src/control/baseControl/selectControl';
import { createL7Icon } from '../src/utils/icon';
class SingleControl extends SelectControl {
public getDefault(option: any): any {
return {
...super.getDefault(option),
options: [
{
icon: createL7Icon('icon-1'),
label: '1',
value: '1',
},
{
icon: createL7Icon('icon-2'),
label: '2',
value: '2',
},
],
defaultValue: '2',
};
}
protected getIsMultiple(): boolean {
return false;
}
}
class MultiControl extends SelectControl {
public getDefault(option: any): any {
return {
...super.getDefault(option),
options: [
{
img: '1',
label: '1',
value: '1',
},
{
img: '1',
label: '2',
value: '2',
},
],
defaultValue: ['2'],
};
}
protected getIsMultiple(): boolean {
return true;
}
}
describe('selectControl', () => {
const scene = TestScene();
it('life cycle', () => {
const control = new SingleControl({});
scene.addControl(control);
const container = control.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
scene.removeControl(control);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
it('normal single select', () => {
const control = new SingleControl({});
scene.addControl(control);
expect(control.getSelectValue()).toEqual('2');
const popperContainer = control.getPopper().getContent() as HTMLDivElement;
const optionDomList = Array.from(
popperContainer.querySelectorAll('.l7-select-control-item'),
) as HTMLDivElement[];
expect(optionDomList).toHaveLength(2);
expect(optionDomList[0].getAttribute('data-option-value')).toEqual('1');
expect(optionDomList[0].getAttribute('data-option-index')).toEqual('0');
optionDomList[0].click();
expect(control.getSelectValue()).toEqual('1');
});
it('img multiple select', () => {
const control = new MultiControl({});
scene.addControl(control);
expect(control.getSelectValue()).toEqual(['2']);
const popperContainer = control.getPopper().getContent() as HTMLDivElement;
const optionDomList = Array.from(
popperContainer.querySelectorAll('.l7-select-control-item'),
) as HTMLDivElement[];
expect(optionDomList).toHaveLength(2);
expect(optionDomList[0].getAttribute('data-option-value')).toEqual('1');
expect(optionDomList[0].getAttribute('data-option-index')).toEqual('0');
expect(popperContainer.querySelectorAll('img')).toHaveLength(2);
optionDomList[0].click();
expect(control.getSelectValue()).toEqual(['2', '1']);
optionDomList[0].click();
optionDomList[1].click();
expect(control.getSelectValue()).toEqual([]);
});
});

View File

@ -1,96 +0,0 @@
import { DOM } from '@antv/l7-utils';
import { createL7Icon } from '../src/utils/icon';
import { Popper } from '../src/utils/popper';
describe('util', () => {
it('icon', () => {
const testClassName = 'l7-test-icon';
const testIcon = createL7Icon(testClassName);
expect(testIcon).toBeInstanceOf(SVGElement);
expect(testIcon.tagName.toLowerCase()).toEqual('svg');
const classList = testIcon.classList;
expect(classList).toContain('l7-iconfont');
});
it('popper', () => {
const button = DOM.create('button') as HTMLButtonElement;
button.innerText = 'Test';
document.body.append(button);
const testContent = '123456';
const popper1 = new Popper(button, {
placement: 'left-start',
trigger: 'click',
content: testContent,
className: 'test-popper-class',
container: document.body,
unique: true,
});
const getPopperClassList = (popper: Popper) => {
return popper.popperDOM.classList;
};
popper1.show();
expect(popper1.getContent()).toEqual(testContent);
expect(getPopperClassList(popper1)).toContain('l7-popper');
expect(getPopperClassList(popper1)).toContain('test-popper-class');
expect(getPopperClassList(popper1)).toContain('l7-popper-left');
expect(getPopperClassList(popper1)).toContain('l7-popper-start');
expect(getPopperClassList(popper1)).not.toContain('l7-popper-hide');
popper1.hide();
button.click();
expect(getPopperClassList(popper1)).not.toContain('l7-popper-hide');
button.click();
expect(getPopperClassList(popper1)).toContain('l7-popper-hide');
const newTestContent = DOM.create('div') as HTMLDivElement;
newTestContent.innerText = '789456';
popper1.setContent(newTestContent);
expect(popper1.contentDOM.firstChild).toEqual(newTestContent);
popper1.show();
const popper2 = new Popper(button, {
placement: 'right-end',
container: document.body,
trigger: 'click',
content: 'hover',
}).show();
expect(getPopperClassList(popper2)).toContain('l7-popper-end');
expect(getPopperClassList(popper2)).toContain('l7-popper-right');
const popper3 = new Popper(button, {
placement: 'top-start',
container: document.body,
trigger: 'click',
content: 'hover',
}).show();
expect(getPopperClassList(popper3)).toContain('l7-popper-top');
expect(getPopperClassList(popper3)).toContain('l7-popper-start');
const popper4 = new Popper(button, {
placement: 'bottom-end',
container: document.body,
trigger: 'click',
content: 'hover',
}).show();
expect(getPopperClassList(popper4)).toContain('l7-popper-bottom');
expect(getPopperClassList(popper4)).toContain('l7-popper-end');
const popper5 = new Popper(button, {
placement: 'left',
container: document.body,
trigger: 'click',
content: 'hover',
}).show();
expect(getPopperClassList(popper5)).toContain('l7-popper-left');
const popper6 = new Popper(button, {
placement: 'top',
container: document.body,
trigger: 'click',
content: 'hover',
}).show();
expect(getPopperClassList(popper6)).toContain('l7-popper-top');
});
});

View File

@ -1,34 +0,0 @@
import { TestScene } from '@antv/l7-test-utils';
import Zoom from '../src/control/zoom';
describe('zoom', () => {
const scene = TestScene();
it('life cycle', () => {
const zoom = new Zoom();
scene.addControl(zoom);
const container = zoom.getContainer();
expect(container.parentElement).toBeInstanceOf(HTMLElement);
scene.removeControl(zoom);
expect(container.parentElement).not.toBeInstanceOf(HTMLElement);
});
it('zoom getDefault', () => {
const zoom = new Zoom();
scene.addControl(zoom);
zoom.disable();
const btnList = Array.from(zoom.getContainer().querySelectorAll('button'));
expect(btnList.map((item) => item.getAttribute('disabled'))).toEqual([
'true',
'true',
]);
zoom.enable();
expect(btnList.map((item) => item.getAttribute('disabled'))).toEqual([
null,
null,
]);
});
});

View File

@ -1,47 +0,0 @@
{
"name": "@antv/l7-component",
"version": "2.11.4",
"description": "",
"main": "lib/index.js",
"module": "es/index.js",
"types": "es/index.d.ts",
"sideEffects": true,
"files": [
"lib",
"es",
"README.md"
],
"scripts": {
"tsc": "tsc --project tsconfig.build.json",
"less": "lessc src/css/index.less src/css/index.css",
"clean": "rimraf dist; rimraf es; rimraf lib;",
"build": "father build",
"build:cjs": "BABEL_ENV=cjs babel src --root-mode upward --out-dir lib --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments",
"build:esm": "BABEL_ENV=esm babel src --root-mode upward --out-dir es --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments",
"watch": "BABEL_ENV=cjs babel src --watch --root-mode upward --out-dir lib --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments",
"lint:ts": "run-p -c lint:ts-*",
"test": "umi-test --passWithNoTests",
"sync": "tnpm sync"
},
"author": "lzxue",
"license": "ISC",
"dependencies": {
"@antv/l7-core": "2.11.4",
"@antv/l7-utils": "2.11.4",
"@babel/runtime": "^7.7.7",
"eventemitter3": "^4.0.0",
"inversify": "^5.0.1",
"lodash": "^4.17.15",
"reflect-metadata": "^0.1.13",
"supercluster": "^7.0.0"
},
"devDependencies": {
"@antv/l7-test-utils": "2.11.4",
"gcoord": "^0.3.2",
"less": "^4.1.3"
},
"gitHead": "684ba4eb806a798713496d3fc0b4d1e17517dc31",
"publishConfig": {
"access": "public"
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,61 +0,0 @@
export const GaodeMapStyleConfig = {
normal: {
text: '标准',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*-nqiT6Vu948AAAAAAAAAAAAAARQnAQ',
},
light: {
text: '月光银',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*J_wYQL_PaUEAAAAAAAAAAAAAARQnAQ',
},
dark: {
text: '幻影黑',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*U7M9QI1yat4AAAAAAAAAAAAAARQnAQ',
},
fresh: {
text: '草色青',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*T-oBT4hB5ucAAAAAAAAAAAAAARQnAQ',
},
grey: {
text: '雅士灰',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*OREXQ4vgQRIAAAAAAAAAAAAAARQnAQ',
},
graffiti: {
text: '涂鸦',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*4UApTKmeiy4AAAAAAAAAAAAAARQnAQ',
},
macaron: {
text: '马卡龙',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*0GrCQLtDjNcAAAAAAAAAAAAAARQnAQ',
},
darkblue: {
text: '极夜蓝',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*uWxqSZQlPkkAAAAAAAAAAAAAARQnAQ',
},
wine: {
text: '酱籽',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*OFPrTbg3an0AAAAAAAAAAAAAARQnAQ',
},
};
export const MapboxMapStyleConfig = {
normal: {
text: '标准',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*AnfJTbIBJOkAAAAAAAAAAAAAARQnAQ',
},
light: {
text: '亮',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*gnuiQIok9qIAAAAAAAAAAAAAARQnAQ',
},
dark: {
text: '暗',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*NwG-TbOlBH0AAAAAAAAAAAAAARQnAQ',
},
satellite: {
text: '卫星',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*2X5EQLKul3IAAAAAAAAAAAAAARQnAQ',
},
outdoors: {
text: '户外',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*gXFLRIaBUI0AAAAAAAAAAAAAARQnAQ',
},
};

View File

@ -1,149 +0,0 @@
import { DOM } from '@antv/l7-utils';
import Control, { IControlOption } from './control';
export { ButtonControl };
export interface IButtonControlOption extends IControlOption {
btnIcon?: DOM.ELType | DocumentFragment;
btnText?: string;
title?: string;
vertical?: boolean;
}
export default class ButtonControl<
O extends IButtonControlOption = IButtonControlOption
> extends Control<O> {
/**
*
* @protected
*/
protected isDisable = false;
/**
* DOM
* @protected
*/
protected button?: HTMLElement;
/**
* DOM
* @protected
*/
protected buttonText?: HTMLElement;
/**
* DOM
* @protected
*/
protected buttonIcon?: DOM.ELType | DocumentFragment;
/**
*
* @param newIsDisable
*/
public setIsDisable(newIsDisable: boolean) {
this.isDisable = newIsDisable;
if (newIsDisable) {
this.button?.setAttribute('disabled', 'true');
} else {
this.button?.removeAttribute('disabled');
}
}
public createButton(className: string = '') {
return DOM.create(
'button',
`l7-button-control ${className}`,
) as HTMLElement;
}
public onAdd(): HTMLElement {
this.button = this.createButton();
this.isDisable = false;
const { title, btnText, btnIcon } = this.controlOption;
this.setBtnTitle(title);
this.setBtnText(btnText);
this.setBtnIcon(btnIcon);
return this.button;
}
public onRemove(): void {
this.button = this.buttonIcon = this.buttonText = undefined;
this.isDisable = false;
}
/**
*
* @param newOptions
*/
public setOptions(newOptions: Partial<O>) {
const { title, btnText, btnIcon } = newOptions;
if (this.checkUpdateOption(newOptions, ['title'])) {
this.setBtnTitle(title);
}
if (this.checkUpdateOption(newOptions, ['btnIcon'])) {
this.setBtnIcon(btnIcon);
}
if (this.checkUpdateOption(newOptions, ['btnText'])) {
this.setBtnText(btnText);
}
super.setOptions(newOptions);
}
/**
* title
* @param title
*/
public setBtnTitle(title: O['title']) {
this.button?.setAttribute('title', title ?? '');
}
/**
* Icon
* @param newIcon
*/
public setBtnIcon(newIcon: O['btnIcon']) {
if (this.buttonIcon) {
DOM.remove(this.buttonIcon);
}
if (newIcon) {
const firstChild = this.button?.firstChild;
if (firstChild) {
this.button?.insertBefore(newIcon, firstChild);
} else {
this.button?.appendChild(newIcon);
}
this.buttonIcon = newIcon;
}
}
/**
*
* @param newText
*/
public setBtnText(newText: O['btnText']) {
if (!this.button) {
return;
}
DOM.removeClass(this.button, 'l7-button-control--row');
DOM.removeClass(this.button, 'l7-button-control--column');
if (newText) {
let btnText = this.buttonText;
if (!btnText) {
btnText = DOM.create('div', 'l7-button-control__text') as HTMLElement;
this.button?.appendChild(btnText);
this.buttonText = btnText;
}
btnText.innerText = newText;
DOM.addClass(
this.button,
this.controlOption.vertical
? 'l7-button-control--column'
: 'l7-button-control--row',
);
} else if (!newText && this.buttonText) {
DOM.remove(this.buttonText);
this.buttonText = undefined;
}
}
}

View File

@ -1,300 +0,0 @@
import {
IControl,
IControlService,
IGlobalConfigService,
ILayerService,
IMapService,
IRendererService,
ISceneService,
PositionName,
PositionType,
TYPES,
} from '@antv/l7-core';
import { DOM } from '@antv/l7-utils';
import EventEmitter from 'eventemitter3';
import { Container } from 'inversify';
import { ControlEvent } from '../../interface';
export { PositionType } from '@antv/l7-core';
export { Control };
export interface IControlOption {
name: string;
position: PositionName | Element;
className?: string;
style?: string;
[key: string]: any;
}
export default class Control<O extends IControlOption = IControlOption>
extends EventEmitter<ControlEvent>
implements IControl<O> {
/**
*
* @protected
*/
protected static controlCount = 0;
/**
*
*/
public controlOption: O;
/**
* DOM
* @protected
*/
protected container: HTMLElement;
/**
*
* @protected
*/
protected isShow: boolean;
protected sceneContainer: Container;
protected scene: ISceneService;
protected mapsService: IMapService;
protected renderService: IRendererService;
protected layerService: ILayerService;
protected controlService: IControlService;
protected configService: IGlobalConfigService;
constructor(option?: Partial<O>) {
super();
Control.controlCount++;
this.controlOption = {
...this.getDefault(option),
...(option || {}),
};
}
public getOptions() {
return this.controlOption;
}
/**
*
* @param newOptions
*/
public setOptions(newOptions: Partial<O>): void {
const defaultOptions = this.getDefault(newOptions);
(Object.entries(newOptions) as Array<[keyof O, any]>).forEach(
([key, value]) => {
if (value === undefined) {
newOptions[key] = defaultOptions[key];
}
},
);
if ('position' in newOptions) {
this.setPosition(newOptions.position);
}
if ('className' in newOptions) {
this.setClassName(newOptions.className);
}
if ('style' in newOptions) {
this.setStyle(newOptions.style);
}
this.controlOption = {
...this.controlOption,
...newOptions,
};
}
/**
* Control Scene controlService
* @param sceneContainer
*/
public addTo(sceneContainer: Container) {
// 初始化各个 Service 实例
this.mapsService = sceneContainer.get<IMapService>(TYPES.IMapService);
this.renderService = sceneContainer.get<IRendererService>(
TYPES.IRendererService,
);
this.layerService = sceneContainer.get<ILayerService>(TYPES.ILayerService);
this.controlService = sceneContainer.get<IControlService>(
TYPES.IControlService,
);
this.configService = sceneContainer.get<IGlobalConfigService>(
TYPES.IGlobalConfigService,
);
this.scene = sceneContainer.get<ISceneService>(TYPES.ISceneService);
this.sceneContainer = sceneContainer;
this.isShow = true;
// 初始化 container
this.container = this.onAdd();
DOM.addClass(this.container, 'l7-control');
const { className, style } = this.controlOption;
if (className) {
this.setClassName(className);
}
if (style) {
this.setStyle(style);
}
// 将 container 插入容器中
this.insertContainer();
this.emit('add', this);
return this;
}
/**
*
*/
public remove() {
if (!this.mapsService) {
return this;
}
DOM.remove(this.container);
this.onRemove();
this.emit('remove', this);
}
/**
* Control Control DOM
*/
public onAdd(): HTMLElement {
return DOM.create('div');
}
/**
* Control
*/
// tslint:disable-next-line:no-empty
public onRemove() {}
/**
*
*/
public show() {
const container = this.container;
DOM.removeClass(container, 'l7-control--hide');
this.isShow = true;
this.emit('show', this);
}
/**
*
*/
public hide() {
const container = this.container;
DOM.addClass(container, 'l7-control--hide');
this.isShow = false;
this.emit('hide', this);
}
/**
*
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getDefault(option?: Partial<O>): O {
// tslint:disable-next-line:no-object-literal-type-assertion
return {
position: PositionType.TOPRIGHT,
name: `${Control.controlCount}`,
} as O;
}
/**
* DOM
*/
public getContainer() {
return this.container;
}
/**
* Control
*/
public getIsShow() {
return this.isShow;
}
public _refocusOnMap(e: MouseEvent) {
// if map exists and event is not a keyboard event
if (this.mapsService && e && e.screenX > 0 && e.screenY > 0) {
const container = this.mapsService.getContainer();
if (container !== null) {
container.focus();
}
}
}
/**
*
* @param position
*/
public setPosition(
position: PositionType | IControlOption['position'] = PositionType.TOPLEFT,
) {
// 考虑组件的自动布局,需要销毁重建
const controlService = this.controlService;
if (controlService) {
controlService.removeControl(this);
}
this.controlOption.position = position;
if (controlService) {
controlService.addControl(this, this.sceneContainer);
}
return this;
}
/**
* container className
* @param className
*/
public setClassName(className?: string | null) {
const container = this.container;
const { className: oldClassName } = this.controlOption;
if (oldClassName) {
DOM.removeClass(container, oldClassName);
}
if (className) {
DOM.addClass(container, className);
}
}
/**
* container style
* @param style
*/
public setStyle(style?: string | null) {
const container = this.container;
if (style) {
container.setAttribute('style', style);
} else {
container.removeAttribute('style');
}
}
/**
* DOM position
* @protected
*/
protected insertContainer() {
const position = this.controlOption.position;
const container = this.container;
if (position instanceof Element) {
position.appendChild(container);
} else {
const corner = this.controlService.controlCorners[position];
if (position.indexOf('bottom') !== -1) {
corner.insertBefore(container, corner.firstChild);
} else {
corner.appendChild(container);
}
}
}
/**
* option keys
* @param option
* @param keys
* @protected
*/
protected checkUpdateOption(option: Partial<O>, keys: Array<keyof O>) {
return keys.some((key) => key in option);
}
}

View File

@ -1,4 +0,0 @@
export * from './control';
export * from './buttonControl';
export * from './popperControl';
export * from './selectControl';

View File

@ -1,115 +0,0 @@
import { PositionName } from '@antv/l7-core';
import { Popper, PopperPlacement, PopperTrigger } from '../../utils/popper';
import ButtonControl, { IButtonControlOption } from './buttonControl';
export { PopperControl };
export interface IPopperControlOption extends IButtonControlOption {
popperPlacement: PopperPlacement;
popperClassName?: string;
popperTrigger: PopperTrigger;
}
const PopperPlacementMap: Record<PositionName, PopperPlacement> = {
topleft: 'right-start',
topcenter: 'bottom',
topright: 'left-start',
bottomleft: 'right-end',
bottomcenter: 'top',
bottomright: 'left-end',
lefttop: 'bottom-start',
leftcenter: 'right',
leftbottom: 'top-start',
righttop: 'bottom-end',
rightcenter: 'left',
rightbottom: 'top-end',
};
export default class PopperControl<
O extends IPopperControlOption = IPopperControlOption
> extends ButtonControl<O> {
/**
*
* @protected
*/
protected popper!: Popper;
public getPopper() {
return this.popper;
}
public hide() {
this.popper.hide();
super.hide();
}
/**
*
* @param option
*/
public getDefault(option?: Partial<O>): O {
const defaultOption = super.getDefault(option);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const position = option?.position ?? defaultOption.position!;
return {
...super.getDefault(option),
popperPlacement:
position instanceof Element ? 'bottom' : PopperPlacementMap[position],
popperTrigger: 'click',
};
}
public onAdd(): HTMLElement {
const button = super.onAdd();
this.initPopper();
return button;
}
public onRemove() {
this.popper.destroy();
}
public initPopper() {
const {
popperClassName,
popperPlacement,
popperTrigger,
} = this.controlOption;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const popperContainer = this.mapsService.getMapContainer()!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.popper = new Popper(this.button!, {
className: popperClassName,
placement: popperPlacement,
trigger: popperTrigger,
container: popperContainer,
unique: true,
});
this.popper
.on('show', () => {
this.emit('popperShow', this);
})
.on('hide', () => {
this.emit('popperHide', this);
});
return this.popper;
}
public setOptions(option: Partial<O>) {
super.setOptions(option);
if (
this.checkUpdateOption(option, [
'popperPlacement',
'popperTrigger',
'popperClassName',
])
) {
const content = this.popper.getContent();
this.popper.destroy();
this.initPopper();
this.popper.setContent(content);
}
}
}

View File

@ -1,221 +0,0 @@
import { DOM } from '@antv/l7-utils';
import { IPopperControlOption, PopperControl } from './popperControl';
type BaseOptionItem = {
value: string;
text: string;
[key: string]: string;
};
type NormalOptionItem = BaseOptionItem & {
icon?: HTMLElement;
};
type ImageOptionItem = BaseOptionItem & {
img: string;
};
export type ControlOptionItem = ImageOptionItem | NormalOptionItem;
export interface ISelectControlOption extends IPopperControlOption {
options: ControlOptionItem[];
defaultValue?: string | string[];
}
export { SelectControl };
enum SelectControlConstant {
ActiveOptionClassName = 'l7-select-control-item-active',
OptionValueAttrKey = 'data-option-value',
OptionIndexAttrKey = 'data-option-index',
}
export default class SelectControl<
O extends ISelectControlOption = ISelectControlOption
> extends PopperControl<O> {
/**
*
* @protected
*/
protected selectValue: string[] = [];
/**
* DOM
* @protected
*/
protected optionDOMList: HTMLElement[];
public setOptions(option: Partial<O>) {
super.setOptions(option);
const { options } = option;
if (options) {
this.popper.setContent(this.getPopperContent(options));
}
}
public onAdd() {
const button = super.onAdd();
const { defaultValue } = this.controlOption;
if (defaultValue) {
this.selectValue = this.transSelectValue(defaultValue);
}
this.popper.setContent(this.getPopperContent(this.controlOption.options));
return button;
}
public getSelectValue() {
return this.getIsMultiple() ? this.selectValue : this.selectValue[0];
}
public setSelectValue(value: string | string[], emitEvent = true) {
const finalValue = this.transSelectValue(value);
this.optionDOMList.forEach((optionDOM) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const optionValue = optionDOM.getAttribute(
SelectControlConstant.OptionValueAttrKey,
)!;
const checkboxDOM = this.getIsMultiple()
? optionDOM.querySelector('input[type=checkbox]')
: undefined;
if (finalValue.includes(optionValue)) {
DOM.addClass(optionDOM, SelectControlConstant.ActiveOptionClassName);
if (checkboxDOM) {
// @ts-ignore
DOM.setChecked(checkboxDOM, true);
}
} else {
DOM.removeClass(optionDOM, SelectControlConstant.ActiveOptionClassName);
if (checkboxDOM) {
// @ts-ignore
DOM.setChecked(checkboxDOM, false);
}
}
});
this.selectValue = finalValue;
if (emitEvent) {
this.emit(
'selectChange',
this.getIsMultiple() ? finalValue : finalValue[0],
);
}
}
/**
*
* @protected
*/
protected getIsMultiple() {
return false;
}
protected getPopperContent(options: ControlOptionItem[]): HTMLElement {
const isImageOptions = this.isImageOptions();
const content = DOM.create(
'div',
isImageOptions ? 'l7-select-control--image' : 'l7-select-control--normal',
) as HTMLElement;
if (this.getIsMultiple()) {
DOM.addClass(content, 'l7-select-control--multiple');
}
const optionsDOMList = options.map((option, optionIndex) => {
const optionDOM = isImageOptions
? // @ts-ignore
this.createImageOption(option)
: this.createNormalOption(option);
optionDOM.setAttribute(
SelectControlConstant.OptionValueAttrKey,
option.value,
);
optionDOM.setAttribute(
SelectControlConstant.OptionIndexAttrKey,
window.String(optionIndex),
);
optionDOM.addEventListener('click', this.onItemClick.bind(this, option));
return optionDOM;
});
content.append(...optionsDOMList);
this.optionDOMList = optionsDOMList;
return content;
}
protected createNormalOption = (option: NormalOptionItem) => {
const isSelect = this.selectValue.includes(option.value);
const optionDOM = DOM.create(
'div',
`l7-select-control-item ${
isSelect ? SelectControlConstant.ActiveOptionClassName : ''
}`,
) as HTMLElement;
if (this.getIsMultiple()) {
optionDOM.appendChild(this.createCheckbox(isSelect));
}
if (option.icon) {
optionDOM.appendChild(option.icon);
}
const textDOM = DOM.create('span');
textDOM.innerText = option.text;
optionDOM.appendChild(textDOM);
return optionDOM;
};
protected createImageOption(option: ImageOptionItem): HTMLElement {
const isSelect = this.selectValue.includes(option.value);
const optionDOM = DOM.create(
'div',
`l7-select-control-item ${
isSelect ? SelectControlConstant.ActiveOptionClassName : ''
}`,
) as HTMLElement;
const imgDOM = DOM.create('img') as HTMLElement;
imgDOM.setAttribute('src', option.img);
DOM.setUnDraggable(imgDOM);
optionDOM.appendChild(imgDOM);
const rowDOM = DOM.create(
'div',
'l7-select-control-item-row',
) as HTMLElement;
if (this.getIsMultiple()) {
optionDOM.appendChild(this.createCheckbox(isSelect));
}
const textDOM = DOM.create('span');
textDOM.innerText = option.text;
rowDOM.appendChild(textDOM);
optionDOM.appendChild(rowDOM);
return optionDOM;
}
protected createCheckbox(isSelect: boolean) {
const checkboxDOM = DOM.create('input') as HTMLElement;
checkboxDOM.setAttribute('type', 'checkbox');
if (isSelect) {
DOM.setChecked(checkboxDOM, true);
}
return checkboxDOM;
}
protected onItemClick = (item: ControlOptionItem) => {
if (this.getIsMultiple()) {
const targetIndex = this.selectValue.findIndex(
(value) => value === item.value,
);
if (targetIndex > -1) {
this.selectValue.splice(targetIndex, 1);
} else {
this.selectValue = [...this.selectValue, item.value];
}
} else {
this.selectValue = [item.value];
}
this.setSelectValue(this.selectValue);
};
protected isImageOptions() {
// @ts-ignore
return !!this.controlOption.options.find((item) => item.img);
}
protected transSelectValue(value: string | string[]) {
return Array.isArray(value) ? value : [value];
}
}

View File

@ -1,71 +0,0 @@
import { createL7Icon } from '../utils/icon';
import ButtonControl, {
IButtonControlOption,
} from './baseControl/buttonControl';
export interface IExportImageControlOption extends IButtonControlOption {
imageType: 'png' | 'jpeg';
onExport: (base64: string) => void;
}
export { ExportImage };
export default class ExportImage extends ButtonControl<IExportImageControlOption> {
public onAdd(): HTMLElement {
const button = super.onAdd();
button.addEventListener('click', this.onClick);
return button;
}
public getDefault(
option?: Partial<IExportImageControlOption>,
): IExportImageControlOption {
return {
...super.getDefault(option),
title: '导出图片',
btnIcon: createL7Icon('l7-icon-export-picture'),
imageType: 'png',
};
}
public getImage() {
const mapImage = this.mapsService.exportMap('png');
const layerImage = this.scene.exportPng('png');
return this.mergeImage(mapImage, layerImage);
}
protected onClick = async () => {
const { onExport } = this.controlOption;
onExport?.(await this.getImage());
};
/**
*
* @protected
* @param base64List
*/
protected mergeImage = async (...base64List: string[]) => {
const { imageType } = this.controlOption;
const { width = 0, height = 0 } =
this.mapsService.getContainer()?.getBoundingClientRect() ?? {};
const canvas = document.createElement('canvas') as HTMLCanvasElement;
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
const imgList = await Promise.all(
base64List.map((base64) => {
return new Promise<HTMLImageElement>((resolve) => {
const img = new Image();
img.onload = () => {
resolve(img);
};
img.src = base64;
});
}),
);
imgList.forEach((img) => {
context?.drawImage(img, 0, 0, width, height);
});
return canvas.toDataURL(`image/${imageType}`) as string;
};
}

View File

@ -1,104 +0,0 @@
import { DOM } from '@antv/l7-utils';
import { createL7Icon } from '../utils/icon';
import ScreenFull from '../utils/screenfull';
import ButtonControl, {
IButtonControlOption,
} from './baseControl/buttonControl';
export interface IFullscreenControlOption extends IButtonControlOption {
exitBtnText: IButtonControlOption['btnText'];
exitBtnIcon: IButtonControlOption['btnIcon'];
exitTitle: IButtonControlOption['title'];
}
export { Fullscreen };
export default class Fullscreen extends ButtonControl<IFullscreenControlOption> {
protected isFullscreen = false;
protected mapContainer: HTMLElement;
constructor(option?: Partial<IFullscreenControlOption>) {
super(option);
if (!ScreenFull.isEnabled) {
console.warn('当前浏览器环境不支持对地图全屏化');
}
}
public setOptions(newOptions: Partial<IFullscreenControlOption>) {
const { exitBtnText, exitBtnIcon, exitTitle } = newOptions;
if (this.isFullscreen) {
if (this.checkUpdateOption(newOptions, ['exitBtnIcon'])) {
this.setBtnIcon(exitBtnIcon);
}
if (this.checkUpdateOption(newOptions, ['exitBtnText'])) {
this.setBtnText(exitBtnText);
}
if (this.checkUpdateOption(newOptions, ['exitTitle'])) {
this.setBtnTitle(exitTitle);
}
}
super.setOptions(newOptions);
}
public onAdd(): HTMLElement {
const button = super.onAdd();
button.addEventListener('click', this.onClick);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.mapContainer = DOM.getContainer(this.scene.getSceneConfig().id!);
this.mapContainer.addEventListener(
'fullscreenchange',
this.onFullscreenChange,
);
return button;
}
public onRemove() {
super.onRemove();
this.mapContainer.removeEventListener(
'fullscreenchange',
this.onFullscreenChange,
);
}
public getDefault(
option?: Partial<IFullscreenControlOption>,
): IFullscreenControlOption {
return {
...super.getDefault(option),
title: '全屏',
btnIcon: createL7Icon('l7-icon-fullscreen'),
exitTitle: '退出全屏',
exitBtnIcon: createL7Icon('l7-icon-exit-fullscreen'),
};
}
public toggleFullscreen = async () => {
if (ScreenFull.isEnabled) {
await ScreenFull.toggle(this.mapContainer);
}
};
protected onClick = () => {
this.toggleFullscreen();
};
protected onFullscreenChange = () => {
this.isFullscreen = !!document.fullscreenElement;
const { btnText, btnIcon, title, exitBtnText, exitBtnIcon, exitTitle } =
this.controlOption;
if (this.isFullscreen) {
this.setBtnTitle(exitTitle);
this.setBtnText(exitBtnText);
this.setBtnIcon(exitBtnIcon);
} else {
this.setBtnTitle(title);
this.setBtnText(btnText);
this.setBtnIcon(btnIcon);
}
this.emit('fullscreenChange', this.isFullscreen);
};
}

View File

@ -1,70 +0,0 @@
import { Point } from '@antv/l7-core';
import { isNaN } from 'lodash';
import { createL7Icon } from '../utils/icon';
import ButtonControl, {
IButtonControlOption,
} from './baseControl/buttonControl';
export interface IGeoLocateOption extends IButtonControlOption {
transform: (position: Point) => Point | Promise<Point>;
}
export { GeoLocate };
export default class GeoLocate extends ButtonControl<IGeoLocateOption> {
constructor(option?: Partial<IGeoLocateOption>) {
super(option);
if (!window.navigator.geolocation) {
console.warn('当前浏览器环境不支持获取地理定位');
}
}
public getDefault(option?: Partial<IGeoLocateOption>): IGeoLocateOption {
return {
...super.getDefault(option),
title: '定位',
btnIcon: createL7Icon('l7-icon-reposition'),
};
}
public onAdd(): HTMLElement {
const button = super.onAdd();
button.addEventListener('click', this.onClick);
return button;
}
/**
* API
*/
public getGeoLocation = () => {
return new Promise<Point>((resolve, reject) => {
window.navigator.geolocation.getCurrentPosition(
({ coords }) => {
const { longitude, latitude } = coords ?? {};
if (!isNaN(longitude) && !isNaN(latitude)) {
resolve([longitude, latitude]);
} else {
reject();
}
},
(e) => {
reject(e);
},
);
});
};
public onClick = async () => {
if (!window.navigator.geolocation) {
return;
}
const { transform } = this.controlOption;
const position = await this.getGeoLocation();
const currentZoom = this.mapsService.getZoom();
this.mapsService.setZoomAndCenter(
currentZoom > 15 ? currentZoom : 15,
transform ? await transform(position) : position,
);
};
}

View File

@ -1,122 +0,0 @@
import { ILayer } from '@antv/l7-core';
import { createL7Icon } from '../utils/icon';
import SelectControl, {
ControlOptionItem,
ISelectControlOption,
} from './baseControl/selectControl';
export interface ILayerSwitchOption extends ISelectControlOption {
layers: Array<ILayer | string>;
}
export { LayerSwitch };
export default class LayerSwitch extends SelectControl<ILayerSwitchOption> {
protected get layers(): ILayer[] {
const layerService = this.layerService;
const { layers } = this.controlOption;
if (Array.isArray(layers) && layers.length) {
const layerInstances: ILayer[] = [];
layers.forEach((layer) => {
if (layer instanceof Object) {
layerInstances.push(layer as ILayer);
}
if (typeof layer === 'string') {
const targetLayer =
layerService.getLayer(layer) || layerService.getLayerByName(layer);
if (targetLayer) {
layerInstances.push(targetLayer);
}
}
});
return layerInstances;
}
return layerService.getLayers() || [];
}
public getDefault(option?: Partial<ILayerSwitchOption>): ILayerSwitchOption {
return {
...super.getDefault(option),
title: '图层控制',
btnIcon: createL7Icon('l7-icon-layer'),
options: [],
};
}
public getLayerVisible() {
return this.layers
.filter((layer) => {
return layer.isVisible();
})
.map((layer) => {
return layer.name;
});
}
public getLayerOptions(): ControlOptionItem[] {
return this.layers.map((layer: ILayer) => {
return {
text: layer.name,
value: layer.name,
};
});
}
public setOptions(option: Partial<ILayerSwitchOption>) {
const isLayerChange = this.checkUpdateOption(option, ['layers']);
super.setOptions(option);
if (isLayerChange) {
this.selectValue = this.getLayerVisible();
this.controlOption.options = this.getLayerOptions();
this.popper.setContent(this.getPopperContent(this.controlOption.options));
}
}
public onAdd(): HTMLElement {
if (!this.controlOption.options?.length) {
this.controlOption.options = this.getLayerOptions();
}
if (!this.controlOption.defaultValue) {
this.controlOption.defaultValue = this.getLayerVisible();
}
this.on('selectChange', this.onSelectChange);
this.layerService.on('layerChange', this.onLayerChange);
return super.onAdd();
}
public onRemove() {
this.off('selectChange', this.onSelectChange);
this.layerService.off('layerChange', this.onLayerChange);
}
protected onLayerChange = () => {
if (this.controlOption.layers?.length) {
return;
}
this.selectValue = this.getLayerVisible();
this.setOptions({
options: this.getLayerOptions(),
});
};
protected onLayerVisibleChane = () => {
this.setSelectValue(this.getLayerVisible());
};
protected onSelectChange = () => {
this.layers.forEach((layer) => {
const needShow = this.selectValue.includes(layer.name);
const isShow = layer.isVisible();
if (needShow && !isShow) {
layer.show();
}
if (!needShow && isShow) {
layer.hide();
}
});
};
protected getIsMultiple(): boolean {
return true;
}
}

View File

@ -1,62 +0,0 @@
import { DOM } from '@antv/l7-utils';
import { Control, IControlOption, PositionType } from './baseControl';
export interface ILogoControlOption extends IControlOption {
// Logo 展示的图片 url
img: string;
// 点击 Logo 跳转的超链接,不传或传 '' | null 则纯展示 Logo点击不跳转
href?: string | null;
}
export { Logo };
export default class Logo extends Control<ILogoControlOption> {
public getDefault(): ILogoControlOption {
return {
position: PositionType.BOTTOMLEFT,
name: 'logo',
href: 'https://l7.antv.antgroup.com/',
img: 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*GRb1TKp4HcMAAAAAAAAAAAAAARQnAQ',
};
}
public onAdd() {
const container = DOM.create('div', 'l7-control-logo');
this.setLogoContent(container);
return container;
}
public onRemove() {
return null;
}
public setOptions(option: Partial<ILogoControlOption>) {
super.setOptions(option);
if (this.checkUpdateOption(option, ['img', 'href'])) {
DOM.clearChildren(this.container);
this.setLogoContent(this.container);
}
}
protected setLogoContent(container: HTMLElement) {
const { href, img } = this.controlOption;
const imgDOM = DOM.create('img') as HTMLElement;
imgDOM.setAttribute('src', img);
imgDOM.setAttribute('aria-label', 'AntV logo');
DOM.setUnDraggable(imgDOM);
if (href) {
const anchorDOM = DOM.create(
'a',
'l7-control-logo-link',
) as HTMLLinkElement;
anchorDOM.target = '_blank';
anchorDOM.href = href;
anchorDOM.rel = 'noopener nofollow';
anchorDOM.setAttribute('rel', 'noopener nofollow');
anchorDOM.appendChild(imgDOM);
container.appendChild(anchorDOM);
} else {
container.appendChild(imgDOM);
}
}
}

View File

@ -1,78 +0,0 @@
import { GaodeMapStyleConfig, MapboxMapStyleConfig } from '../constants';
import { createL7Icon } from '../utils/icon';
import SelectControl, {
ControlOptionItem,
ISelectControlOption,
} from './baseControl/selectControl';
export { MapTheme };
export default class MapTheme extends SelectControl<ISelectControlOption> {
public getDefault(
option?: Partial<ISelectControlOption>,
): ISelectControlOption {
return {
...super.getDefault(option),
title: '地图样式',
btnIcon: createL7Icon('l7-icon-color'),
options: [],
};
}
public getStyleOptions(): ControlOptionItem[] {
const mapStyleConfig =
this.mapsService.getType() === 'mapbox'
? MapboxMapStyleConfig
: GaodeMapStyleConfig;
return Object.entries(this.mapsService.getMapStyleConfig())
.filter(([key, value]) => typeof value === 'string' && key !== 'blank')
.map(([key, value]) => {
// @ts-ignore
const { text, img } = mapStyleConfig[key] ?? {};
return {
text: text ?? key,
value,
img,
key,
};
});
}
public getMapStyle() {
return this.mapsService.getMapStyle();
}
public onAdd(): HTMLElement {
if (!this.controlOption.options?.length) {
this.controlOption.options = this.getStyleOptions();
}
if (this.controlOption.defaultValue) {
const defaultValue = this.controlOption.defaultValue as string;
this.controlOption.defaultValue =
this.controlOption.options.find((item) => item.key === defaultValue)
?.value ?? defaultValue;
} else {
const defaultStyle = this.getMapStyle();
if (defaultStyle) {
this.controlOption.defaultValue = defaultStyle;
} else {
// @ts-ignore
this.mapsService.map.once('styledata', () => {
const mapboxStyle = this.mapsService.getMapStyle();
this.controlOption.defaultValue = mapboxStyle;
this.setSelectValue(mapboxStyle, false);
});
}
}
this.on('selectChange', this.onMapThemeChange);
return super.onAdd();
}
protected onMapThemeChange = () => {
this.mapsService.setMapStyle(this.selectValue[0]);
};
protected getIsMultiple(): boolean {
return false;
}
}

View File

@ -1,58 +0,0 @@
import { ILngLat, Position, PositionType } from '@antv/l7-core';
import { DOM } from '@antv/l7-utils';
import Control, { IControlOption } from './baseControl/control';
export interface IMouseLocationControlOption extends IControlOption {
transform: (position: Position) => Position;
}
export { MouseLocation };
export default class MouseLocation extends Control<IMouseLocationControlOption> {
protected location: Position = [0, 0];
public getLocation() {
return this.location;
}
public getDefault(
option?: Partial<IMouseLocationControlOption>,
): IMouseLocationControlOption {
return {
...super.getDefault(option),
position: PositionType.BOTTOMLEFT,
transform: ([lng, lat]) => {
return [+(+lng).toFixed(6), +(+lat).toFixed(6)];
},
};
}
public onAdd(): HTMLElement {
const container = DOM.create('div', 'l7-control-mouse-location');
container.innerHTML = '&nbsp;';
this.mapsService.on('mousemove', this.onMouseMove);
return container;
}
public onRemove(): void {
this.mapsService.off('mousemove', this.onMouseMove);
}
protected onMouseMove = (e: any) => {
let position: Position = this.location;
const lngLat: ILngLat | undefined = e.lngLat || e.lnglat;
const { transform } = this.controlOption;
if (lngLat) {
position = [lngLat.lng, lngLat.lat];
}
this.location = position;
if (transform) {
position = transform(position);
}
this.insertLocation2HTML(position);
this.emit('locationChange', position);
};
protected insertLocation2HTML(position: Position) {
this.container.innerText = position.join(', ');
}
}

View File

@ -1,136 +0,0 @@
import { DOM, lnglatDistance } from '@antv/l7-utils';
import { Control, IControlOption, PositionType } from './baseControl';
export interface IScaleControlOption extends IControlOption {
lockWidth: boolean;
maxWidth: number;
metric: boolean;
updateWhenIdle: boolean;
imperial: boolean;
}
export { Scale };
export default class Scale extends Control<IScaleControlOption> {
private mScale: HTMLElement;
private iScale: HTMLElement;
public getDefault(option: Partial<IScaleControlOption>) {
return {
...super.getDefault(option),
name: 'scale',
position: PositionType.BOTTOMLEFT,
maxWidth: 100,
metric: true,
updateWhenIdle: false,
imperial: false,
lockWidth: true,
};
}
public onAdd() {
const className = 'l7-control-scale';
const container = DOM.create('div', className);
this.resetScaleLines(container);
const { updateWhenIdle } = this.controlOption;
this.mapsService.on(updateWhenIdle ? 'moveend' : 'mapmove', this.update);
this.mapsService.on(updateWhenIdle ? 'zoomend' : 'zoomchange', this.update);
return container;
}
public onRemove() {
const { updateWhenIdle } = this.controlOption;
this.mapsService.off(
updateWhenIdle ? 'zoomend' : 'zoomchange',
this.update,
);
this.mapsService.off(updateWhenIdle ? 'moveend' : 'mapmove', this.update);
}
public setOptions(newOption: Partial<IScaleControlOption>) {
super.setOptions(newOption);
if (
this.checkUpdateOption(newOption, [
'lockWidth',
'maxWidth',
'metric',
'updateWhenIdle',
'imperial',
])
) {
this.resetScaleLines(this.container);
}
}
public update = () => {
const mapsService = this.mapsService;
const { maxWidth } = this.controlOption;
const y = mapsService.getSize()[1] / 2;
const p1 = mapsService.containerToLngLat([0, y]);
const p2 = mapsService.containerToLngLat([maxWidth, y]);
const maxMeters = lnglatDistance([p1.lng, p1.lat], [p2.lng, p2.lat]);
this.updateScales(maxMeters);
};
public updateScales(maxMeters: number) {
const { metric, imperial } = this.controlOption;
if (metric && maxMeters) {
this.updateMetric(maxMeters);
}
if (imperial && maxMeters) {
this.updateImperial(maxMeters);
}
}
private resetScaleLines(container: HTMLElement) {
DOM.clearChildren(container);
const { metric, imperial, maxWidth, lockWidth } = this.controlOption;
if (lockWidth) {
DOM.addStyle(container, `width: ${maxWidth}px`);
}
if (metric) {
this.mScale = DOM.create('div', 'l7-control-scale-line', container);
}
if (imperial) {
this.iScale = DOM.create('div', 'l7-control-scale-line', container);
}
this.update();
}
private updateScale(scale: HTMLElement, text: string, ratio: number) {
const { maxWidth } = this.controlOption;
scale.style.width = Math.round(maxWidth * ratio) + 'px';
scale.innerHTML = text;
}
private getRoundNum(num: number) {
const pow10 = Math.pow(10, (Math.floor(num) + '').length - 1);
let d = num / pow10;
d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1;
return pow10 * d;
}
private updateMetric(maxMeters: number) {
const meters = this.getRoundNum(maxMeters);
const label = meters < 1000 ? meters + ' m' : meters / 1000 + ' km';
this.updateScale(this.mScale, label, meters / maxMeters);
}
private updateImperial(maxMeters: number) {
const maxFeet = maxMeters * 3.2808399;
let maxMiles: number;
let miles: number;
let feet: number;
if (maxFeet > 5280) {
maxMiles = maxFeet / 5280;
miles = this.getRoundNum(maxMiles);
this.updateScale(this.iScale, miles + ' mi', miles / maxMiles);
} else {
feet = this.getRoundNum(maxFeet);
this.updateScale(this.iScale, feet + ' ft', feet / maxFeet);
}
}
}

View File

@ -1,136 +0,0 @@
import { PositionType } from '@antv/l7-core';
import { DOM } from '@antv/l7-utils';
import { createL7Icon } from '../utils/icon';
import { Control, IControlOption } from './baseControl';
export interface IZoomControlOption extends IControlOption {
zoomInText: DOM.ELType | string;
zoomInTitle: string;
zoomOutText: DOM.ELType | string;
zoomOutTitle: string;
}
export { Zoom };
export default class Zoom extends Control<IZoomControlOption> {
private disabled: boolean;
private zoomInButton: HTMLElement;
private zoomOutButton: HTMLElement;
public getDefault(option: Partial<IZoomControlOption>) {
return {
...super.getDefault(option),
position: PositionType.BOTTOMRIGHT,
name: 'zoom',
zoomInText: createL7Icon('l7-icon-enlarge'),
zoomInTitle: 'Zoom in',
zoomOutText: createL7Icon('l7-icon-narrow'),
zoomOutTitle: 'Zoom out',
};
}
public setOptions(newOptions: Partial<IZoomControlOption>) {
super.setOptions(newOptions);
if (
this.checkUpdateOption(newOptions, [
'zoomInText',
'zoomInTitle',
'zoomOutText',
'zoomOutTitle',
])
) {
this.resetButtonGroup(this.container);
}
}
public onAdd(): HTMLElement {
const container = DOM.create('div', 'l7-control-zoom');
this.resetButtonGroup(container);
this.mapsService.on('zoomend', this.updateDisabled);
this.mapsService.on('zoomchange', this.updateDisabled);
return container;
}
public onRemove() {
this.mapsService.off('zoomend', this.updateDisabled);
this.mapsService.off('zoomchange', this.updateDisabled);
}
public disable() {
this.disabled = true;
this.updateDisabled();
return this;
}
public enable() {
this.disabled = false;
this.updateDisabled();
return this;
}
public zoomIn = () => {
if (
!this.disabled &&
this.mapsService.getZoom() < this.mapsService.getMaxZoom()
) {
this.mapsService.zoomIn();
}
};
public zoomOut = () => {
if (
!this.disabled &&
this.mapsService.getZoom() > this.mapsService.getMinZoom()
) {
this.mapsService.zoomOut();
}
};
private resetButtonGroup(container: HTMLElement) {
DOM.clearChildren(container);
this.zoomInButton = this.createButton(
this.controlOption.zoomInText,
this.controlOption.zoomInTitle,
'l7-button-control',
container,
this.zoomIn,
);
this.zoomOutButton = this.createButton(
this.controlOption.zoomOutText,
this.controlOption.zoomOutTitle,
'l7-button-control',
container,
this.zoomOut,
);
this.updateDisabled();
}
private createButton(
html: DOM.ELType | string,
tile: string,
className: string,
container: HTMLElement,
fn: (...arg: any[]) => any,
) {
const link = DOM.create('button', className, container) as HTMLLinkElement;
if (typeof html === 'string') {
link.innerHTML = html;
} else {
link.append(html);
}
link.title = tile;
link.addEventListener('click', fn);
return link;
}
private updateDisabled = () => {
const mapsService = this.mapsService;
this.zoomInButton.removeAttribute('disabled');
this.zoomOutButton.removeAttribute('disabled');
if (this.disabled || mapsService.getZoom() <= mapsService.getMinZoom()) {
this.zoomOutButton.setAttribute('disabled', 'true');
}
if (this.disabled || mapsService.getZoom() >= mapsService.getMaxZoom()) {
this.zoomInButton.setAttribute('disabled', 'true');
}
};
}

View File

@ -1,70 +0,0 @@
@import 'variables.less';
.l7-button-control {
min-width: @l7-btn-control-size;
height: @l7-btn-control-size;
background-color: @l7-control-bg-color;
border-width: 0;
border-radius: @l7-btn-control-border-radius;
outline: 0;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: center;
align-items: center;
padding: 0 ((@l7-btn-control-size - @l7-btn-icon-size) / 2);
box-shadow: @l7-control-shadow;
line-height: 16px;
.l7-iconfont {
fill: @l7-control-font-color;
color: @l7-control-font-color;
width: @l7-btn-icon-size;
height: @l7-btn-icon-size;
}
&.l7-button-control--row {
padding: 0 16px 0 13px;
* + .l7-button-control__text {
margin-left: 8px;
}
}
&.l7-button-control--column {
height: @l7-btn-column-height;
flex-direction: column;
.l7-iconfont {
margin-top: 3px;
}
.l7-button-control__text {
margin-top: 3px;
font-size: 10px;
transform: scale(0.83333);
}
}
&:not(:disabled) {
&:hover {
background-color: @l7-btn-control-bg-hover-color;
}
&:active {
background-color: @l7-btn-control-bg-active-color;
}
}
&:disabled {
background-color: @l7-btn-control-disabled-bg-color;
color: @l7-btn-control-disabled-font-color;
cursor: not-allowed;
.l7-iconfont {
fill: @l7-btn-control-disabled-font-color;
color: @l7-btn-control-disabled-font-color;
}
&:hover {
background-color: @l7-btn-control-disabled-bg-color;
}
&:active {
background-color: @l7-btn-control-disabled-bg-color;
}
}
}

View File

@ -1,71 +0,0 @@
@import 'variables.less';
.l7-control-container {
font: 12px/1.5 'Helvetica Neue', Arial, Helvetica, sans-serif;
.l7-control {
position: relative;
z-index: 800;
float: left;
clear: both;
color: @l7-control-font-color;
font-size: @l7-control-font-size;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
&.l7-control--hide {
display: none;
}
}
each(@position-list,{
.l7-@{value} {
@{value}: 0;
display: flex;
position: absolute;
z-index: 1000;
pointer-events: none;
.l7-control:not(.l7-control--hide) {
margin-@{value}: @l7-control-space;
}
}
});
.l7-center {
position: absolute;
display: flex;
justify-content: center;
&.l7-top,
&.l7-bottom {
width: 100%;
}
&.l7-left,
&.l7-right {
height: 100%;
}
.l7-control {
margin-right: @l7-control-space;
margin-bottom: @l7-control-space;
}
}
.l7-row {
flex-direction: row;
&.l7-top {
align-items: flex-start;
}
&.l7-bottom {
align-items: flex-end;
}
}
.l7-column {
flex-direction: column;
&.l7-left {
align-items: flex-start;
}
&.l7-right {
align-items: flex-end;
}
}
}

View File

@ -1,586 +0,0 @@
.l7-marker-container {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
.l7-marker {
position: absolute !important;
top: 0;
left: 0;
z-index: 5;
cursor: pointer;
}
.l7-marker-cluster {
width: 40px;
height: 40px;
background-color: rgba(181, 226, 140, 0.6);
background-clip: padding-box;
border-radius: 20px;
}
.l7-marker-cluster div {
width: 30px;
height: 30px;
margin-top: 5px;
margin-left: 5px;
font: 12px 'Helvetica Neue', Arial, Helvetica, sans-serif;
text-align: center;
background-color: rgba(110, 204, 57, 0.6);
border-radius: 15px;
}
.l7-marker-cluster span {
line-height: 30px;
}
.l7-touch .l7-control-attribution,
.l7-touch .l7-control-layers,
.l7-touch .l7-bar {
box-shadow: none;
}
.l7-touch .l7-control-layers,
.l7-touch .l7-bar {
background-clip: padding-box;
border: 2px solid rgba(0, 0, 0, 0.2);
}
.mapboxgl-ctrl-logo,
.amap-logo {
display: none !important;
}
.l7-select-box {
border: 3px dashed gray;
border-radius: 2px;
position: absolute;
z-index: 1000;
box-sizing: border-box;
}
.l7-control-container {
font: 12px/1.5 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.l7-control-container .l7-control {
position: relative;
z-index: 800;
float: left;
clear: both;
color: #595959;
font-size: 12px;
pointer-events: visiblePainted;
/* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.l7-control-container .l7-control.l7-control--hide {
display: none;
}
.l7-control-container .l7-top {
top: 0;
display: flex;
position: absolute;
z-index: 1000;
pointer-events: none;
}
.l7-control-container .l7-top .l7-control:not(.l7-control--hide) {
margin-top: 8px;
}
.l7-control-container .l7-right {
right: 0;
display: flex;
position: absolute;
z-index: 1000;
pointer-events: none;
}
.l7-control-container .l7-right .l7-control:not(.l7-control--hide) {
margin-right: 8px;
}
.l7-control-container .l7-bottom {
bottom: 0;
display: flex;
position: absolute;
z-index: 1000;
pointer-events: none;
}
.l7-control-container .l7-bottom .l7-control:not(.l7-control--hide) {
margin-bottom: 8px;
}
.l7-control-container .l7-left {
left: 0;
display: flex;
position: absolute;
z-index: 1000;
pointer-events: none;
}
.l7-control-container .l7-left .l7-control:not(.l7-control--hide) {
margin-left: 8px;
}
.l7-control-container .l7-center {
position: absolute;
display: flex;
justify-content: center;
}
.l7-control-container .l7-center.l7-top,
.l7-control-container .l7-center.l7-bottom {
width: 100%;
}
.l7-control-container .l7-center.l7-left,
.l7-control-container .l7-center.l7-right {
height: 100%;
}
.l7-control-container .l7-center .l7-control {
margin-right: 8px;
margin-bottom: 8px;
}
.l7-control-container .l7-row {
flex-direction: row;
}
.l7-control-container .l7-row.l7-top {
align-items: flex-start;
}
.l7-control-container .l7-row.l7-bottom {
align-items: flex-end;
}
.l7-control-container .l7-column {
flex-direction: column;
}
.l7-control-container .l7-column.l7-left {
align-items: flex-start;
}
.l7-control-container .l7-column.l7-right {
align-items: flex-end;
}
.l7-button-control {
min-width: 28px;
height: 28px;
background-color: #fff;
border-width: 0;
border-radius: 2px;
outline: 0;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: center;
align-items: center;
padding: 0 6px;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
line-height: 16px;
}
.l7-button-control .l7-iconfont {
fill: #595959;
color: #595959;
width: 16px;
height: 16px;
}
.l7-button-control.l7-button-control--row {
padding: 0 16px 0 13px;
}
.l7-button-control.l7-button-control--row * + .l7-button-control__text {
margin-left: 8px;
}
.l7-button-control.l7-button-control--column {
height: 44px;
flex-direction: column;
}
.l7-button-control.l7-button-control--column .l7-iconfont {
margin-top: 3px;
}
.l7-button-control.l7-button-control--column .l7-button-control__text {
margin-top: 3px;
font-size: 10px;
transform: scale(0.83333);
}
.l7-button-control:not(:disabled):hover {
background-color: #f3f3f3;
}
.l7-button-control:not(:disabled):active {
background-color: #f3f3f3;
}
.l7-button-control:disabled {
background-color: #fafafa;
color: #bdbdbd;
cursor: not-allowed;
}
.l7-button-control:disabled .l7-iconfont {
fill: #bdbdbd;
color: #bdbdbd;
}
.l7-button-control:disabled:hover {
background-color: #fafafa;
}
.l7-button-control:disabled:active {
background-color: #fafafa;
}
.l7-popper {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
z-index: 5;
color: #595959;
}
.l7-popper.l7-popper-hide {
display: none;
}
.l7-popper .l7-popper-content {
min-height: 28px;
background: #fff;
border-radius: 2px;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
}
.l7-popper .l7-popper-arrow {
width: 0;
height: 0;
border-width: 4px;
border-style: solid;
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
border-right-color: transparent;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
}
.l7-popper.l7-popper-left {
flex-direction: row;
}
.l7-popper.l7-popper-left .l7-popper-arrow {
border-left-color: #fff;
margin: 10px 0;
}
.l7-popper.l7-popper-right {
flex-direction: row-reverse;
}
.l7-popper.l7-popper-right .l7-popper-arrow {
border-right-color: #fff;
margin: 10px 0;
}
.l7-popper.l7-popper-top {
flex-direction: column;
}
.l7-popper.l7-popper-top .l7-popper-arrow {
border-top-color: #fff;
margin: 0 10px;
}
.l7-popper.l7-popper-bottom {
flex-direction: column-reverse;
}
.l7-popper.l7-popper-bottom .l7-popper-arrow {
border-bottom-color: #fff;
margin: 0 10px;
}
.l7-popper.l7-popper-start {
align-items: flex-start;
}
.l7-popper.l7-popper-end {
align-items: flex-end;
}
.l7-select-control--normal {
padding: 4px 0;
}
.l7-select-control--normal .l7-select-control-item {
height: 24px;
line-height: 24px;
display: flex;
align-items: center;
padding: 0 16px;
font-size: 12px;
}
.l7-select-control--normal .l7-select-control-item > * + * {
margin-left: 6px;
}
.l7-select-control--normal .l7-select-control-item input[type='checkbox'] {
height: 14px;
width: 14px;
}
.l7-select-control--normal .l7-select-control-item:hover {
background-color: #f3f3f3;
}
.l7-select-control--image {
padding: 12px 12px 0 12px;
width: 474px;
height: 320px;
overflow: auto;
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
align-items: flex-start;
}
.l7-select-control--image .l7-select-control-item {
margin-right: 12px;
border-radius: 2px;
overflow: hidden;
border: 1px solid #fff;
box-sizing: content-box;
width: calc((100% - 36px) / 3);
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 12px;
position: relative;
font-size: 12px;
}
.l7-select-control--image .l7-select-control-item img {
width: 142px;
height: 80px;
}
.l7-select-control--image .l7-select-control-item input[type='checkbox'] {
position: absolute;
right: 0;
top: 0;
}
.l7-select-control--image .l7-select-control-item .l7-select-control-item-row {
display: flex;
justify-content: center;
align-items: center;
line-height: 26px;
}
.l7-select-control--image .l7-select-control-item .l7-select-control-item-row > * + * {
margin-left: 8px;
}
.l7-select-control--image .l7-select-control-item.l7-select-control-item-active {
border-color: #0370fe;
}
.l7-select-control--image .l7-select-control-item:nth-child(3n) {
margin-right: 0;
}
.l7-select-control-item {
cursor: pointer;
}
.l7-select-control-item input[type='checkbox'] {
margin: 0;
cursor: pointer;
}
.l7-select-control--multiple .l7-select-control-item:hover {
background-color: transparent;
}
.l7-control-logo {
width: 89px;
height: 16px;
user-select: none;
}
.l7-control-logo img {
height: 100%;
width: 100%;
}
.l7-control-logo .l7-control-logo-link {
display: block;
cursor: pointer;
}
.l7-control-logo .l7-control-logo-link img {
cursor: pointer;
}
.l7-control-mouse-location {
background-color: #fff;
border-radius: 2px;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
padding: 2px 4px;
min-width: 130px;
}
.l7-control-zoom {
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
border-radius: 2px;
overflow: hidden;
}
.l7-control-zoom .l7-button-control {
box-shadow: 0 0 0;
border-radius: 0;
font-size: 16px;
}
.l7-control-zoom .l7-button-control .l7-iconfont {
width: 14px;
height: 14px;
}
.l7-control-zoom .l7-button-control:first-child {
border-bottom: 1px solid #f0f0f0;
}
.l7-control-scale {
display: flex;
flex-direction: column;
}
.l7-control-scale .l7-control-scale-line {
box-sizing: border-box;
padding: 2px 5px 1px;
overflow: hidden;
color: #595959;
font-size: 10px;
line-height: 1.1;
white-space: nowrap;
background: #fff;
border: 2px solid #000;
border-top: 0;
transition: width 0.1s;
}
.l7-control-scale .l7-control-scale-line + .l7-control-scale .l7-control-scale-line {
margin-top: -2px;
border-top: 2px solid #777;
border-bottom: none;
}
.l7-right .l7-control-scale {
display: flex;
align-items: flex-end;
}
.l7-right .l7-control-scale .l7-control-scale-line {
text-align: right;
}
.l7-popup {
position: absolute;
top: 0;
left: 0;
z-index: 5;
display: -webkit-flex;
display: flex;
will-change: transform;
pointer-events: none;
}
.l7-popup.l7-popup-hide {
display: none;
}
.l7-popup .l7-popup-content {
position: relative;
padding: 16px;
font-size: 14px;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.l7-popup .l7-popup-content .l7-popup-content__title {
margin-bottom: 8px;
font-weight: bold;
}
.l7-popup .l7-popup-content .l7-popup-close-button,
.l7-popup .l7-popup-content .l7-popup-content__title,
.l7-popup .l7-popup-content .l7-popup-content__panel {
white-space: normal;
user-select: text;
pointer-events: initial;
}
.l7-popup .l7-popup-content .l7-popup-close-button {
position: absolute;
top: 0;
right: 0;
width: 18px;
height: 18px;
padding: 0;
font-size: 14px;
line-height: 18px;
text-align: center;
background-color: transparent;
border: 0;
border-radius: 0 3px 0 0;
cursor: pointer;
}
.l7-popup .l7-popup-tip {
position: relative;
z-index: 1;
width: 0;
height: 0;
border: 10px solid transparent;
}
.l7-popup.l7-popup-anchor-bottom,
.l7-popup.l7-popup-anchor-bottom-left,
.l7-popup.l7-popup-anchor-bottom-right {
-webkit-flex-direction: column-reverse;
flex-direction: column-reverse;
}
.l7-popup.l7-popup-anchor-bottom .l7-popup-tip,
.l7-popup.l7-popup-anchor-bottom-left .l7-popup-tip,
.l7-popup.l7-popup-anchor-bottom-right .l7-popup-tip {
bottom: 1px;
}
.l7-popup.l7-popup-anchor-top,
.l7-popup.l7-popup-anchor-top-left,
.l7-popup.l7-popup-anchor-top-right {
-webkit-flex-direction: column;
flex-direction: column;
}
.l7-popup.l7-popup-anchor-top .l7-popup-tip,
.l7-popup.l7-popup-anchor-top-left .l7-popup-tip,
.l7-popup.l7-popup-anchor-top-right .l7-popup-tip {
top: 1px;
}
.l7-popup.l7-popup-anchor-left {
-webkit-flex-direction: row;
flex-direction: row;
}
.l7-popup.l7-popup-anchor-right {
-webkit-flex-direction: row-reverse;
flex-direction: row-reverse;
}
.l7-popup-anchor-top .l7-popup-tip {
position: relative;
-webkit-align-self: center;
align-self: center;
border-top: none;
border-bottom-color: #fff;
}
.l7-popup-anchor-top-left .l7-popup-tip {
-webkit-align-self: flex-start;
align-self: flex-start;
border-top: none;
border-bottom-color: #fff;
border-left: none;
}
.l7-popup-anchor-top-right .l7-popup-tip {
-webkit-align-self: flex-end;
align-self: flex-end;
border-top: none;
border-right: none;
border-bottom-color: #fff;
}
.l7-popup-anchor-bottom .l7-popup-tip {
-webkit-align-self: center;
align-self: center;
border-top-color: #fff;
border-bottom: none;
}
.l7-popup-anchor-bottom-left .l7-popup-tip {
-webkit-align-self: flex-start;
align-self: flex-start;
border-top-color: #fff;
border-bottom: none;
border-left: none;
}
.l7-popup-anchor-bottom-right .l7-popup-tip {
-webkit-align-self: flex-end;
align-self: flex-end;
border-top-color: #fff;
border-right: none;
border-bottom: none;
}
.l7-popup-anchor-left .l7-popup-tip {
-webkit-align-self: center;
align-self: center;
border-right-color: #fff;
border-left: none;
}
.l7-popup-anchor-right .l7-popup-tip {
right: 1px;
-webkit-align-self: center;
align-self: center;
border-right: none;
border-left-color: #fff;
}
.l7-popup-anchor-top-left .l7-popup-content {
border-top-left-radius: 0;
}
.l7-popup-anchor-top-right .l7-popup-content {
border-top-right-radius: 0;
}
.l7-popup-anchor-bottom-left .l7-popup-content {
border-bottom-left-radius: 0;
}
.l7-popup-anchor-bottom-right .l7-popup-content {
border-bottom-right-radius: 0;
}
.l7-popup-track-pointer {
display: none;
}
.l7-popup-track-pointer * {
user-select: none;
pointer-events: none;
}
.l7-map:hover .l7-popup-track-pointer {
display: flex;
}
.l7-map:active .l7-popup-track-pointer {
display: none;
}
.l7-layer-popup__row {
font-size: 12px;
}
.l7-layer-popup__row + .l7-layer-popup__row {
margin-top: 4px;
}

View File

@ -1,12 +0,0 @@
@import 'variables';
@import 'l7';
@import 'control';
@import 'button';
@import 'popper';
@import 'select';
@import 'logo';
@import 'mouseLocation';
@import 'zoom';
@import 'scale';
@import 'popup';
@import 'layerPopup';

View File

@ -1,60 +0,0 @@
.l7-marker-container {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
.l7-marker {
position: absolute !important;
top: 0;
left: 0;
z-index: 5;
cursor: pointer;
}
.l7-marker-cluster {
width: 40px;
height: 40px;
background-color: rgba(181, 226, 140, 0.6);
background-clip: padding-box;
border-radius: 20px;
}
.l7-marker-cluster div {
width: 30px;
height: 30px;
margin-top: 5px;
margin-left: 5px;
font: 12px 'Helvetica Neue', Arial, Helvetica, sans-serif;
text-align: center;
background-color: rgba(110, 204, 57, 0.6);
border-radius: 15px;
}
.l7-marker-cluster span {
line-height: 30px;
}
.l7-touch .l7-control-attribution,
.l7-touch .l7-control-layers,
.l7-touch .l7-bar {
box-shadow: none;
}
.l7-touch .l7-control-layers,
.l7-touch .l7-bar {
background-clip: padding-box;
border: 2px solid rgba(0, 0, 0, 0.2);
}
// 隐藏底图 Logo
.mapboxgl-ctrl-logo,
.amap-logo {
display: none !important;
}
.l7-select-box {
border: 3px dashed gray;
border-radius: 2px;
position: absolute;
z-index: 1000;
box-sizing: border-box;
}

View File

@ -1,8 +0,0 @@
@import 'variables';
.l7-layer-popup__row {
font-size: 12px;
& + & {
margin-top: 4px;
}
}

View File

@ -1,18 +0,0 @@
@import 'variables';
.l7-control-logo {
width: 89px;
height: 16px;
user-select: none;
img {
height: 100%;
width: 100%;
}
.l7-control-logo-link {
display: block;
cursor: pointer;
img {
cursor: pointer;
}
}
}

View File

@ -1,9 +0,0 @@
@import 'variables';
.l7-control-mouse-location {
background-color: @l7-control-bg-color;
border-radius: @l7-btn-control-border-radius;
box-shadow: @l7-control-shadow;
padding: 2px 4px;
min-width: 130px;
}

View File

@ -1,64 +0,0 @@
@import 'variables';
.l7-popper {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
z-index: 5;
color: @l7-control-font-color;
&.l7-popper-hide {
display: none;
}
.l7-popper-content {
min-height: @l7-btn-control-size;
background: @l7-popper-control-bg-color;
border-radius: @l7-btn-control-border-radius;
box-shadow: @l7-control-shadow;
}
.l7-popper-arrow {
width: 0;
height: 0;
border-width: @l7-popper-control-arrow-size;
border-style: solid;
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
border-right-color: transparent;
box-shadow: @l7-control-shadow;
}
&.l7-popper-left {
flex-direction: row;
.l7-popper-arrow {
border-left-color: @l7-popper-control-bg-color;
margin: (@l7-btn-control-size / 2 - @l7-popper-control-arrow-size) 0;
}
}
&.l7-popper-right {
flex-direction: row-reverse;
.l7-popper-arrow {
border-right-color: @l7-popper-control-bg-color;
margin: (@l7-btn-control-size / 2 - @l7-popper-control-arrow-size) 0;
}
}
&.l7-popper-top {
flex-direction: column;
.l7-popper-arrow {
border-top-color: @l7-popper-control-bg-color;
margin: 0 (@l7-btn-control-size / 2 - @l7-popper-control-arrow-size);
}
}
&.l7-popper-bottom {
flex-direction: column-reverse;
.l7-popper-arrow {
border-bottom-color: @l7-popper-control-bg-color;
margin: 0 (@l7-btn-control-size / 2 - @l7-popper-control-arrow-size);
}
}
&.l7-popper-start {
align-items: flex-start;
}
&.l7-popper-end {
align-items: flex-end;
}
}

View File

@ -1,185 +0,0 @@
@import 'variables';
.l7-popup {
position: absolute;
top: 0;
left: 0;
z-index: 5;
display: -webkit-flex;
display: flex;
will-change: transform;
pointer-events: none;
&.l7-popup-hide {
display: none;
}
.l7-popup-content {
position: relative;
padding: 16px;
font-size: 14px;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
.l7-popup-content__title {
margin-bottom: 8px;
font-weight: bold;
}
.l7-popup-close-button,
.l7-popup-content__title,
.l7-popup-content__panel {
white-space: normal;
user-select: text;
pointer-events: initial;
}
.l7-popup-close-button {
position: absolute;
top: 0;
right: 0;
width: 18px;
height: 18px;
padding: 0;
font-size: 14px;
line-height: 18px;
text-align: center;
background-color: transparent;
border: 0;
border-radius: 0 3px 0 0;
cursor: pointer;
}
}
.l7-popup-tip {
position: relative;
z-index: 1;
width: 0;
height: 0;
border: 10px solid transparent;
}
&.l7-popup-anchor-bottom,
&.l7-popup-anchor-bottom-left,
&.l7-popup-anchor-bottom-right {
-webkit-flex-direction: column-reverse;
flex-direction: column-reverse;
.l7-popup-tip {
bottom: 1px;
}
}
&.l7-popup-anchor-top,
&.l7-popup-anchor-top-left,
&.l7-popup-anchor-top-right {
-webkit-flex-direction: column;
flex-direction: column;
.l7-popup-tip {
top: 1px;
}
}
&.l7-popup-anchor-left {
-webkit-flex-direction: row;
flex-direction: row;
}
&.l7-popup-anchor-right {
-webkit-flex-direction: row-reverse;
flex-direction: row-reverse;
}
}
.l7-popup-anchor-top .l7-popup-tip {
position: relative;
-webkit-align-self: center;
align-self: center;
border-top: none;
border-bottom-color: #fff;
}
.l7-popup-anchor-top-left .l7-popup-tip {
-webkit-align-self: flex-start;
align-self: flex-start;
border-top: none;
border-bottom-color: #fff;
border-left: none;
}
.l7-popup-anchor-top-right .l7-popup-tip {
-webkit-align-self: flex-end;
align-self: flex-end;
border-top: none;
border-right: none;
border-bottom-color: #fff;
}
.l7-popup-anchor-bottom .l7-popup-tip {
-webkit-align-self: center;
align-self: center;
border-top-color: #fff;
border-bottom: none;
}
.l7-popup-anchor-bottom-left .l7-popup-tip {
-webkit-align-self: flex-start;
align-self: flex-start;
border-top-color: #fff;
border-bottom: none;
border-left: none;
}
.l7-popup-anchor-bottom-right .l7-popup-tip {
-webkit-align-self: flex-end;
align-self: flex-end;
border-top-color: #fff;
border-right: none;
border-bottom: none;
}
.l7-popup-anchor-left .l7-popup-tip {
-webkit-align-self: center;
align-self: center;
border-right-color: #fff;
border-left: none;
}
.l7-popup-anchor-right .l7-popup-tip {
right: 1px;
-webkit-align-self: center;
align-self: center;
border-right: none;
border-left-color: #fff;
}
.l7-popup-anchor-top-left .l7-popup-content {
border-top-left-radius: 0;
}
.l7-popup-anchor-top-right .l7-popup-content {
border-top-right-radius: 0;
}
.l7-popup-anchor-bottom-left .l7-popup-content {
border-bottom-left-radius: 0;
}
.l7-popup-anchor-bottom-right .l7-popup-content {
border-bottom-right-radius: 0;
}
.l7-popup-track-pointer {
display: none;
* {
user-select: none;
pointer-events: none;
}
}
.l7-map:hover .l7-popup-track-pointer {
display: flex;
}
.l7-map:active .l7-popup-track-pointer {
display: none;
}

View File

@ -1,34 +0,0 @@
@import 'variables';
.l7-control-scale {
display: flex;
flex-direction: column;
.l7-control-scale-line {
box-sizing: border-box;
padding: 2px 5px 1px;
overflow: hidden;
color: @l7-control-font-color;
font-size: 10px;
line-height: 1.1;
white-space: nowrap;
background: @l7-control-bg-color;
border: 2px solid #000;
border-top: 0;
transition: width 0.1s;
& + & {
margin-top: -2px;
border-top: 2px solid #777;
border-bottom: none;
}
}
}
.l7-right {
.l7-control-scale {
display: flex;
align-items: flex-end;
.l7-control-scale-line {
text-align: right;
}
}
}

View File

@ -1,86 +0,0 @@
@import 'variables';
.l7-select-control--normal {
padding: 4px 0;
.l7-select-control-item {
height: 24px;
line-height: 24px;
display: flex;
align-items: center;
padding: 0 16px;
font-size: 12px;
> * + * {
margin-left: 6px;
}
input[type='checkbox'] {
height: 14px;
width: 14px;
}
&:hover {
background-color: @l7-btn-control-bg-hover-color;
}
}
}
.l7-select-control--image {
padding: 12px 12px 0 12px;
width: @l7-select-control-image-popper-width;
height: 320px;
overflow: auto;
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
align-items: flex-start;
.l7-select-control-item {
margin-right: 12px;
border-radius: @l7-btn-control-border-radius;
overflow: hidden;
border: 1px solid @l7-popper-control-bg-color;
box-sizing: content-box;
width: calc((100% - 36px) / 3);
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 12px;
position: relative;
font-size: 12px;
img {
width: 142px;
height: 80px;
}
input[type='checkbox'] {
position: absolute;
right: 0;
top: 0;
}
.l7-select-control-item-row {
display: flex;
justify-content: center;
align-items: center;
line-height: 26px;
> * + * {
margin-left: 8px;
}
}
&.l7-select-control-item-active {
border-color: @l7-select-control-active-color;
}
&:nth-child(3n) {
margin-right: 0;
}
}
}
.l7-select-control-item {
cursor: pointer;
input[type='checkbox'] {
margin: 0;
cursor: pointer;
}
}
.l7-select-control--multiple {
.l7-select-control-item:hover {
background-color: transparent;
}
}

View File

@ -1,28 +0,0 @@
// Control
@l7-control-space: 8px;
@l7-control-font-size: 12px;
@l7-control-font-color: #595959;
@l7-control-bg-color: #fff;
@l7-control-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
// ButtonControl
@l7-btn-control-bg-color: @l7-control-bg-color;
@l7-btn-control-bg-hover-color: #f3f3f3;
@l7-btn-control-bg-active-color: @l7-btn-control-bg-hover-color;
@l7-btn-control-size: 28px;
@l7-btn-icon-size: 16px;
@l7-btn-control-border-radius: 2px;
@l7-btn-control-disabled-bg-color: #fafafa;
@l7-btn-control-disabled-font-color: #bdbdbd;
@l7-btn-border-color: #f0f0f0;
@l7-btn-column-height: 44px;
// PopperControl
@l7-popper-control-bg-color: @l7-btn-control-bg-color;
@l7-popper-control-arrow-size: 4px;
// SelectControl
@l7-select-control-active-color: #0370fe;
@l7-select-control-image-popper-width: 474px;
@position-list: top, right, bottom, left;

View File

@ -1,21 +0,0 @@
@import 'variables';
@zoom-icon-size: 14px;
.l7-control-zoom {
box-shadow: @l7-control-shadow;
border-radius: @l7-btn-control-border-radius;
overflow: hidden;
.l7-button-control {
box-shadow: 0 0 0;
border-radius: 0;
font-size: @l7-btn-icon-size;
.l7-iconfont {
width: @zoom-icon-size;
height: @zoom-icon-size;
}
&:first-child {
border-bottom: 1px solid @l7-btn-border-color;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" class="icon" p-id="8341" t="1566292427369" version="1.1" viewBox="0 0 1024 1024"><defs><style type="text/css"/></defs><path fill="#000" d="M256 341.333333l256 128 256-128-256-128-256 128z m276.864-208.384l341.034667 173.909334c20.736 10.581333 28.202667 34.56 16.682666 53.632a41.386667 41.386667 0 0 1-16.64 15.317333l-341.077333 173.909333a46.336 46.336 0 0 1-41.728 0L150.101333 375.808c-20.736-10.581333-28.202667-34.56-16.682666-53.632a41.386667 41.386667 0 0 1 16.64-15.317333l341.077333-173.909334c12.970667-6.613333 28.757333-6.613333 41.728 0z m0 587.349334a45.653333 45.653333 0 0 1-41.728 0l-341.034667-176.938667c-20.736-10.752-28.202667-35.157333-16.682666-54.528a41.642667 41.642667 0 0 1 16.64-15.573333 34.901333 34.901333 0 0 1 32.213333 0l308.906667 160.213333c12.928 6.741333 28.714667 6.741333 41.685333 0l308.864-160.213333a34.901333 34.901333 0 0 1 32.170667 0c20.736 10.752 28.202667 35.157333 16.682666 54.528a41.642667 41.642667 0 0 1-16.64 15.573333l-341.077333 176.938667z m0 170.666666a45.653333 45.653333 0 0 1-41.728 0l-341.034667-176.938666c-20.736-10.752-28.202667-35.157333-16.682666-54.528a41.642667 41.642667 0 0 1 16.64-15.573334 34.901333 34.901333 0 0 1 32.213333 0l308.906667 160.213334c12.928 6.741333 28.714667 6.741333 41.685333 0l308.864-160.213334a34.901333 34.901333 0 0 1 32.170667 0c20.736 10.752 28.202667 35.157333 16.682666 54.528a41.642667 41.642667 0 0 1-16.64 15.573334l-341.077333 176.938666z" p-id="8342"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Created with Vectornator (http://vectornator.io/) -->
<svg height="100%" stroke-miterlimit="10" style="fill-rule:nonzero;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" version="1.1" viewBox="0 0 682.67 682.67" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:vectornator="http://vectornator.io" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/>
<clipPath id="ArtboardFrame">
<rect height="682.67" width="682.67" x="0" y="0"/>
</clipPath>
<g clip-path="url(#ArtboardFrame)" id="Untitled" vectornator:layerName="Untitled">
<path d="M0 477.87L0 580.27C0.00552233 636.822 45.8483 682.664 102.4 682.67L204.8 682.67C223.424 682.351 238.356 667.162 238.356 648.535C238.356 629.908 223.424 614.719 204.8 614.4L102.4 614.4C83.5505 614.4 68.27 599.119 68.27 580.27L68.27 477.87C68.4812 465.535 62.0216 454.046 51.3734 447.817C40.7252 441.588 27.5448 441.588 16.8966 447.817C6.24842 454.046-0.211108 465.535 0 477.87ZM477.87 682.67L580.27 682.67C636.822 682.664 682.664 636.822 682.67 580.27L682.67 477.87C682.881 465.535 676.422 454.046 665.773 447.817C655.125 441.588 641.945 441.588 631.297 447.817C620.648 454.046 614.189 465.535 614.4 477.87L614.4 580.27C614.4 599.119 599.119 614.4 580.27 614.4L477.87 614.4C465.535 614.189 454.046 620.648 447.817 631.297C441.588 641.945 441.588 655.125 447.817 665.773C454.046 676.422 465.535 682.881 477.87 682.67ZM682.67 204.8L682.67 102.4C682.664 45.8483 636.822 0.00553344 580.27 1.15748e-06L477.87 1.15748e-06C465.535-0.211139 454.046 6.24838 447.817 16.8966C441.588 27.5448 441.588 40.7252 447.817 51.3734C454.046 62.0216 465.535 68.4811 477.87 68.27L580.27 68.27C599.119 68.27 614.4 83.5505 614.4 102.4L614.4 204.8C614.719 223.424 629.908 238.356 648.535 238.356C667.162 238.356 682.351 223.424 682.67 204.8ZM204.8 0L102.4 0C45.8483 0.00551165 0.00552271 45.8483-3.66902e-07 102.4L-3.66902e-07 204.8C0.318807 223.424 15.5078 238.356 34.135 238.356C52.7622 238.356 67.9512 223.424 68.27 204.8L68.27 102.4C68.27 83.5505 83.5505 68.27 102.4 68.27L204.8 68.27C223.424 67.9512 238.356 52.7622 238.356 34.135C238.356 15.5078 223.424 0.318804 204.8 0Z" fill="#333333" fill-rule="nonzero" opacity="1" stroke="none"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,23 +0,0 @@
import Marker from './marker';
import MarkerLayer from './marker-layer';
import './assets/iconfont/iconfont.js';
// 引入样式
import './css/index.css';
export * from './control/baseControl';
export * from './control/logo';
export * from './control/fullscreen';
export * from './control/exportImage';
export * from './control/geoLocate';
export * from './control/mapTheme';
export * from './control/layerSwitch';
export * from './control/mouseLocation';
export * from './control/zoom';
export * from './control/scale';
export * from './popup/popup';
export * from './popup/layerPopup';
export { Marker, MarkerLayer };
export * from './interface';

View File

@ -1,18 +0,0 @@
export type ControlEvent = 'show' | 'hide' | 'add' | 'remove' | string;
export interface IMarkerStyleOption {
element?: (...args: any[]) => any;
style: { [key: string]: any } | ((...args: any[]) => any);
className: string;
field?: string;
method?: 'sum' | 'max' | 'min' | 'mean';
radius: number;
maxZoom: number;
minZoom: number;
zoom: number;
}
export interface IMarkerLayerOption {
cluster: boolean;
clusterOption: Partial<IMarkerStyleOption>;
}

View File

@ -1,327 +0,0 @@
import {
IMapService,
IMarker,
IMarkerContainerAndBounds,
TYPES,
} from '@antv/l7-core';
import {
bindAll,
boundsContains,
DOM,
IBounds,
padBounds,
Satistics,
} from '@antv/l7-utils';
import { EventEmitter } from 'eventemitter3';
import { Container } from 'inversify';
import { merge } from 'lodash';
// @ts-ignore
// tslint:disable-next-line:no-submodule-imports
import Supercluster from 'supercluster/dist/supercluster';
import { IMarkerLayerOption, IMarkerStyleOption } from './interface';
import Marker from './marker';
interface IPointFeature {
geometry: {
type: 'Point';
coordinates: [number, number];
};
properties: any;
}
export default class MarkerLayer extends EventEmitter {
private markers: IMarker[] = []; // 原始的marker列表
private markerLayerOption: IMarkerLayerOption;
private clusterIndex: Supercluster;
private points: IPointFeature[] = [];
private clusterMarkers: IMarker[] = []; // 聚合后的marker列表
private mapsService: IMapService<unknown>;
private scene: Container;
private zoom: number;
private bbox: IBounds;
private inited: boolean;
private containerSize: IMarkerContainerAndBounds;
constructor(option?: Partial<IMarkerLayerOption>) {
super();
this.markerLayerOption = merge(this.getDefault(), option);
bindAll(['update'], this);
this.zoom = this.markerLayerOption.clusterOption?.zoom || -99;
}
public getDefault() {
return {
cluster: false,
clusterOption: {
radius: 80,
maxZoom: 20,
minZoom: 0,
zoom: -99,
style: {},
className: '',
},
};
}
// 执行scene.addMarkerLayer时调用
public addTo(scene: Container) {
// this.remove();
this.scene = scene;
this.mapsService = scene.get<IMapService>(TYPES.IMapService);
if (this.markerLayerOption.cluster) {
this.initCluster();
this.update();
// 地图视野变化时,重新计算视野内的聚合点。
this.mapsService.on('camerachange', this.update); // amap1.x 更新事件
this.mapsService.on('viewchange', this.update); // amap2.0 更新事件
}
this.mapsService.on('camerachange', this.setContainerSize.bind(this)); // amap1.x 更新事件
this.mapsService.on('viewchange', this.setContainerSize.bind(this)); // amap2.0 更新事件
this.addMarkers();
this.inited = true;
return this;
}
// 设置容器大小
private setContainerSize() {
if (!this.mapsService) return;
const container = this.mapsService.getContainer();
this.containerSize = {
containerWidth: container?.scrollWidth || 0,
containerHeight: container?.scrollHeight || 0,
bounds: this.mapsService.getBounds(),
};
}
// 获取容器尺寸
private getContainerSize() {
return this.containerSize;
}
// 在图层添加单个marker
public addMarker(marker: IMarker) {
const cluster = this.markerLayerOption.cluster;
marker.getMarkerLayerContainerSize = this.getContainerSize.bind(this);
if (cluster) {
this.addPoint(marker, this.markers.length);
if (this.mapsService) {
// 在新增 marker 的时候需要更新聚合信息(哪怕此时的 zoom 没有发生变化)
const zoom = this.mapsService.getZoom();
const bbox = this.mapsService.getBounds();
this.bbox = padBounds(bbox, 0.5);
this.zoom = Math.floor(zoom);
this.getClusterMarker(this.bbox, this.zoom);
}
}
this.markers.push(marker);
// if(this.inited) {
// marker.addTo(this.scene);
// }
}
public removeMarker(marker: IMarker) {
this.markers.indexOf(marker);
const markerIndex = this.markers.indexOf(marker);
if (markerIndex > -1) {
this.markers.splice(markerIndex, 1);
}
}
/**
* marker marker markerContainer markerContainer
*/
public hide() {
this.markers.map((m) => {
m.getElement().style.opacity = '0';
});
this.clusterMarkers.map((m) => {
m.getElement().style.opacity = '0';
});
}
/**
* marker
*/
public show() {
this.markers.map((m) => {
m.getElement().style.opacity = '1';
});
this.clusterMarkers.map((m) => {
m.getElement().style.opacity = '1';
});
}
// 返回当下的markers数据有聚合图时返回聚合的marker列表否则返回原始maerker列表
public getMarkers() {
const cluster = this.markerLayerOption.cluster;
return cluster ? this.clusterMarkers : this.markers;
}
// 批量添加marker到scene
public addMarkers() {
this.getMarkers().forEach((marker: IMarker) => {
marker.addTo(this.scene);
});
}
// 清除图层里的marker
public clear() {
this.markers.forEach((marker: IMarker) => {
marker.remove();
});
this.clusterMarkers.forEach((clusterMarker: IMarker) => {
clusterMarker.remove();
});
this.markers = [];
this.points = [];
this.clusterMarkers = [];
}
public destroy() {
this.clear();
this.removeAllListeners();
this.mapsService.off('camerachange', this.update);
this.mapsService.off('viewchange', this.update);
this.mapsService.off('camerachange', this.setContainerSize.bind(this));
this.mapsService.off('viewchange', this.setContainerSize.bind(this));
}
// 将marker数据保存在point中
private addPoint(marker: IMarker, id: number) {
const { lng, lat } = marker.getLnglat();
const feature: IPointFeature = {
geometry: {
type: 'Point',
coordinates: [lng, lat],
},
properties: {
...marker.getExtData(),
marker_id: id,
},
};
this.points.push(feature);
if (this.clusterIndex) {
// 在新增点的时候需要更新 cluster 的数据
this.clusterIndex.load(this.points);
}
}
private initCluster() {
if (!this.markerLayerOption.cluster) {
return;
}
const {
radius,
minZoom = 0,
maxZoom,
} = this.markerLayerOption.clusterOption;
this.clusterIndex = new Supercluster({
radius,
minZoom,
maxZoom,
});
// @ts-ignore
this.clusterIndex.load(this.points);
}
private getClusterMarker(viewBounds: IBounds, zoom: number) {
const viewBBox = viewBounds[0].concat(viewBounds[1]);
const clusterPoint = this.clusterIndex.getClusters(viewBBox, zoom);
this.clusterMarkers.forEach((marker: IMarker) => {
marker.remove();
});
this.clusterMarkers = [];
clusterPoint.forEach((feature: any) => {
const { field, method } = this.markerLayerOption.clusterOption;
// 处理聚合数据
if (feature.properties?.cluster_id) {
const clusterData = this.getLeaves(feature.properties?.cluster_id);
feature.properties.clusterData = clusterData;
if (field && method) {
const columnData = clusterData?.map((item: any) => {
const data = {
[field]: item.properties[field],
};
return data;
});
const column = Satistics.getColumn(columnData as any, field);
const stat = Satistics.getSatByColumn(method, column);
const fieldName = 'point_' + method;
feature.properties[fieldName] = stat.toFixed(2);
}
}
const marker = this.clusterMarker(feature);
this.clusterMarkers.push(marker);
marker.addTo(this.scene);
});
}
private getLeaves(
clusterId: number,
limit: number = Infinity,
offset: number = 0,
) {
if (!clusterId) {
return null;
}
return this.clusterIndex.getLeaves(clusterId, limit, offset);
}
private clusterMarker(feature: any) {
const clusterOption = this.markerLayerOption.clusterOption;
const {
element = this.generateElement.bind(this),
} = clusterOption as IMarkerStyleOption;
const marker = new Marker({
element: element(feature),
}).setLnglat({
lng: feature.geometry.coordinates[0],
lat: feature.geometry.coordinates[1],
});
return marker;
}
private normalMarker(feature: any) {
const marker_id = feature.properties.marker_id;
return this.markers[marker_id];
}
private update() {
if (!this.mapsService) return;
// 当图层中无marker时无需更新
if (this.markers.length === 0) return;
const zoom = this.mapsService.getZoom();
const bbox = this.mapsService.getBounds();
if (
!this.bbox ||
Math.abs(zoom - this.zoom) >= 1 ||
!boundsContains(this.bbox, bbox)
) {
this.bbox = padBounds(bbox, 0.5);
this.zoom = Math.floor(zoom);
this.getClusterMarker(this.bbox, this.zoom);
}
}
private generateElement(feature: any) {
const el = DOM.create('div', 'l7-marker-cluster');
const label = DOM.create('div', '', el);
const span = DOM.create('span', '', label);
const { field, method } = this.markerLayerOption.clusterOption;
feature.properties.point_count = feature.properties.point_count || 1;
const text =
field && method
? feature.properties['point_' + method] || feature.properties[field]
: feature.properties.point_count;
span.textContent = text;
return el;
}
}

View File

@ -1,359 +0,0 @@
import {
ILngLat,
IMapService,
IMarkerContainerAndBounds,
IMarkerOption,
IPoint,
IPopup,
ISceneService,
TYPES,
} from '@antv/l7-core';
import {
anchorTranslate,
anchorType,
applyAnchorClass,
bindAll,
DOM,
} from '@antv/l7-utils';
import { EventEmitter } from 'eventemitter3';
import { Container } from 'inversify';
// marker 支持 dragger 未完成
export default class Marker extends EventEmitter {
private markerOption: IMarkerOption;
private defaultMarker: boolean;
private popup: IPopup;
private mapsService: IMapService<unknown>;
private sceneSerive: ISceneService;
private lngLat: ILngLat;
private scene: Container;
private added: boolean = false;
public getMarkerLayerContainerSize(): IMarkerContainerAndBounds | void {}
constructor(option?: Partial<IMarkerOption>) {
super();
this.markerOption = {
...this.getDefault(),
...option,
};
bindAll(['update', 'onMove', 'onMapClick'], this);
this.init();
}
public getDefault() {
return {
element: undefined, // DOM element
anchor: anchorType.BOTTOM,
offsets: [0, 0],
color: '#5B8FF9',
draggable: false,
};
}
public addTo(scene: Container) {
// this.remove();
this.scene = scene;
this.mapsService = scene.get<IMapService>(TYPES.IMapService);
this.sceneSerive = scene.get<ISceneService>(TYPES.ISceneService);
const { element } = this.markerOption;
// this.sceneSerive.getSceneContainer().appendChild(element as HTMLElement);
this.mapsService.getMarkerContainer().appendChild(element as HTMLElement);
this.registerMarkerEvent(element as HTMLElement);
this.mapsService.on('camerachange', this.update); // 注册高德1.x 的地图事件监听
this.mapsService.on('viewchange', this.update); // 注册高德2.0 的地图事件监听
this.update();
this.added = true;
this.emit('added');
return this;
}
public remove() {
if (this.mapsService) {
this.mapsService.off('click', this.onMapClick);
this.mapsService.off('move', this.update);
this.mapsService.off('moveend', this.update);
// this.mapsService.off('mousedown', this.addDragHandler);
// this.mapsService.off('touchstart', this.addDragHandler);
// this.mapsService.off('mouseup', this.onUp);
// this.mapsService.off('touchend', this.onUp);
}
this.unRegisterMarkerEvent();
this.removeAllListeners();
const { element } = this.markerOption;
if (element) {
DOM.remove(element);
}
if (this.popup) {
this.popup.remove();
}
return this;
}
public setLnglat(lngLat: ILngLat | IPoint) {
this.lngLat = lngLat as ILngLat;
if (Array.isArray(lngLat)) {
this.lngLat = {
lng: lngLat[0],
lat: lngLat[1],
};
}
if (this.popup) {
this.popup.setLnglat(this.lngLat);
}
this.update();
return this;
}
public getLnglat(): ILngLat {
return this.lngLat;
}
public getElement(): HTMLElement {
return this.markerOption.element as HTMLElement;
}
public setElement(el: HTMLElement): this {
if (!this.added) {
this.once('added', () => {
this.setElement(el);
});
return this;
}
const { element } = this.markerOption;
if (element) {
DOM.remove(element);
}
this.markerOption.element = el;
this.init();
this.mapsService.getMarkerContainer().appendChild(el as HTMLElement);
this.registerMarkerEvent(el as HTMLElement);
this.update();
return this;
}
public openPopup(): this {
if (!this.added) {
this.once('added', () => {
this.openPopup();
});
return this;
}
const popup = this.popup;
if (!popup) {
return this;
}
if (!popup.isOpen()) {
popup.addTo(this.scene);
}
return this;
}
public closePopup(): this {
if (!this.added) {
this.once('added', () => {
this.closePopup();
});
}
const popup = this.popup;
if (popup) {
popup.remove();
}
return this;
}
public setPopup(popup: IPopup) {
this.popup = popup;
if (this.lngLat) {
this.popup.setLnglat(this.lngLat);
}
return this;
}
public togglePopup() {
const popup = this.popup;
if (!popup) {
return this;
} else if (popup.isOpen()) {
popup.remove();
} else {
popup.addTo(this.scene);
}
return this;
}
public getPopup() {
return this.popup;
}
public getOffset(): number[] {
return this.markerOption.offsets;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public setDraggable(draggable: boolean) {
throw new Error('Method not implemented.');
}
public isDraggable() {
return this.markerOption.draggable;
}
public getExtData() {
return this.markerOption.extData;
}
public setExtData(data: any) {
this.markerOption.extData = data;
}
private update() {
if (!this.mapsService) {
return;
}
const { element, anchor } = this.markerOption;
this.updatePosition();
DOM.setTransform(element as HTMLElement, `${anchorTranslate[anchor]}`);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private onMapClick(e: MouseEvent) {
const { element } = this.markerOption;
if (this.popup && element) {
this.togglePopup();
}
}
private getCurrentContainerSize() {
const container = this.mapsService.getContainer();
return {
containerHeight: container?.scrollHeight || 0,
containerWidth: container?.scrollWidth || 0,
bounds: this.mapsService.getBounds(),
};
}
private updatePosition() {
if (!this.mapsService) {
return;
}
const { element, offsets } = this.markerOption;
const { lng, lat } = this.lngLat;
const pos = this.mapsService.lngLatToContainer([lng, lat]);
if (element) {
element.style.display = 'block';
element.style.whiteSpace = 'nowrap';
const { containerHeight, containerWidth, bounds } =
this.getMarkerLayerContainerSize() || this.getCurrentContainerSize();
if (!bounds) return;
// 当前可视区域包含跨日界线
if (Math.abs(bounds[0][0]) > 180 || Math.abs(bounds[1][0]) > 180) {
if (pos.x > containerWidth) {
// 日界线右侧点左移
const newPos = this.mapsService.lngLatToContainer([lng - 360, lat]);
pos.x = newPos.x;
}
if (pos.x < 0) {
// 日界线左侧点右移
const newPos = this.mapsService.lngLatToContainer([lng + 360, lat]);
pos.x = newPos.x;
}
}
// 不在当前可视区域内隐藏点
if (
pos.x > containerWidth ||
pos.x < 0 ||
pos.y > containerHeight ||
pos.y < 0
) {
element.style.display = 'none';
}
element.style.left = pos.x + offsets[0] + 'px';
element.style.top = pos.y - offsets[1] + 'px';
}
}
private init() {
let { element } = this.markerOption;
const { color, anchor } = this.markerOption;
if (!element) {
this.defaultMarker = true;
element = DOM.create('div') as HTMLDivElement;
this.markerOption.element = element;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttributeNS(null, 'display', 'block');
svg.setAttributeNS(null, 'height', '48px');
svg.setAttributeNS(null, 'width', '48px');
svg.setAttributeNS(null, 'viewBox', '0 0 1024 1024');
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path',
);
path.setAttributeNS(
null,
'd',
'M512 490.666667C453.12 490.666667 405.333333 442.88 405.333333 384 405.333333 325.12 453.12 277.333333 512 277.333333 570.88 277.333333 618.666667 325.12 618.666667 384 618.666667 442.88 570.88 490.666667 512 490.666667M512 85.333333C346.88 85.333333 213.333333 218.88 213.333333 384 213.333333 608 512 938.666667 512 938.666667 512 938.666667 810.666667 608 810.666667 384 810.666667 218.88 677.12 85.333333 512 85.333333Z',
);
path.setAttributeNS(null, 'fill', color);
svg.appendChild(path);
element.appendChild(svg);
}
DOM.addClass(element, 'l7-marker');
Object.keys(this.markerOption.style || {}).forEach(
// @ts-ignore
(key: keyof CSSStyleDeclaration) => {
const value =
this.markerOption?.style && (this.markerOption?.style[key] as string);
if (element) {
// @ts-ignore
(element.style as CSSStyleDeclaration)[key] = value;
}
},
);
element.addEventListener('click', (e: MouseEvent) => {
this.onMapClick(e);
});
element.addEventListener('click', this.eventHandle);
applyAnchorClass(element, anchor, 'marker');
}
private registerMarkerEvent(element: HTMLElement) {
element.addEventListener('mousemove', this.eventHandle);
element.addEventListener('click', this.eventHandle);
element.addEventListener('mousedown', this.eventHandle);
element.addEventListener('mouseup', this.eventHandle);
element.addEventListener('dblclick', this.eventHandle);
element.addEventListener('contextmenu', this.eventHandle);
element.addEventListener('mouseover', this.eventHandle);
element.addEventListener('mouseout', this.eventHandle);
}
private unRegisterMarkerEvent() {
const element = this.getElement();
element.removeEventListener('mousemove', this.eventHandle);
element.removeEventListener('click', this.eventHandle);
element.removeEventListener('mousedown', this.eventHandle);
element.removeEventListener('mouseup', this.eventHandle);
element.removeEventListener('dblclick', this.eventHandle);
element.removeEventListener('contextmenu', this.eventHandle);
element.removeEventListener('mouseover', this.eventHandle);
element.removeEventListener('mouseout', this.eventHandle);
}
private eventHandle = (e: MouseEvent) => {
this.emit(e.type, {
target: e,
data: this.markerOption.extData,
lngLat: this.lngLat,
});
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private addDragHandler(e: MouseEvent) {
return null
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private onUp(e: MouseEvent) {
throw new Error('Method not implemented.');
}
}

View File

@ -1,312 +0,0 @@
import { ILayer, IPopupOption } from '@antv/l7-core';
import { DOM } from '@antv/l7-utils';
import { Container } from 'inversify';
import { get } from 'lodash';
// import { Container } from 'inversify';
import Popup from './popup';
type ElementType = DOM.ElementType;
export type LayerField = {
field: string;
formatField?: ElementType | ((field: string, feature: any) => ElementType);
formatValue?: ElementType | ((value: any, feature: any) => ElementType);
getValue?: (feature: any) => any;
};
export type LayerPopupConfigItem = {
layer: ILayer | string;
fields?: Array<LayerField | string>;
title?: ElementType | ((feature: any) => ElementType);
customContent?: ElementType | ((feature: any) => ElementType);
};
export interface ILayerPopupOption extends IPopupOption {
config?: LayerPopupConfigItem[];
items?: LayerPopupConfigItem[];
trigger: 'hover' | 'click';
}
type LayerMapInfo = {
onMouseMove?: (layer: ILayer, e: any) => void;
onMouseOut?: (layer: ILayer, e: any) => void;
onClick?: (layer: ILayer, e: any) => void;
onSourceUpdate?: (layer: ILayer) => void;
} & Partial<LayerPopupConfigItem>;
export { LayerPopup };
export default class LayerPopup extends Popup<ILayerPopupOption> {
/**
*
* @protected
*/
protected layerConfigMap: WeakMap<ILayer, LayerMapInfo> = new WeakMap();
/**
* id
* @protected
*/
protected displayFeatureInfo?: {
layer: ILayer;
featureId: number;
};
protected get layerConfigItems() {
const { config, items } = this.popupOption;
return config ?? items ?? [];
}
public addTo(scene: Container) {
super.addTo(scene);
this.bindLayerEvent();
this.hide();
return this;
}
public remove() {
super.remove();
this.unbindLayerEvent();
return this;
}
public setOptions(option: Partial<ILayerPopupOption>) {
this.unbindLayerEvent();
super.setOptions(option);
this.bindLayerEvent();
return this;
}
protected getDefault(option: Partial<ILayerPopupOption>): ILayerPopupOption {
const isClickTrigger = option.trigger === 'click';
return {
...super.getDefault(option),
trigger: 'hover',
followCursor: !isClickTrigger,
lngLat: {
lng: 0,
lat: 0,
},
offsets: [0, 10],
closeButton: false,
closeOnClick: false,
autoClose: false,
closeOnEsc: false,
};
}
/**
*
* @protected
*/
protected bindLayerEvent() {
const { trigger } = this.popupOption;
this.layerConfigItems.forEach((configItem) => {
const layer = this.getLayerByConfig(configItem);
if (!layer) {
return;
}
const layerInfo: LayerMapInfo = {
...configItem,
};
if (trigger === 'hover') {
const onMouseMove = this.onLayerMouseMove.bind(this, layer);
const onMouseOut = this.onLayerMouseOut.bind(this, layer);
layerInfo.onMouseMove = onMouseMove;
layerInfo.onMouseOut = onMouseOut;
layer?.on('mousemove', onMouseMove);
layer?.on('mouseout', onMouseOut);
} else {
const onClick = this.onLayerClick.bind(this, layer);
layerInfo.onClick = onClick;
layer?.on('click', onClick);
}
const source = layer?.getSource?.();
const onSourceUpdate = this.onSourceUpdate.bind(this, layer);
source?.on('update', onSourceUpdate);
layerInfo.onSourceUpdate = onSourceUpdate;
this.layerConfigMap.set(layer, layerInfo);
});
}
/**
*
* @protected
*/
protected unbindLayerEvent() {
this.layerConfigItems.forEach((configItem) => {
const layer = this.getLayerByConfig(configItem);
const layerInfo = layer && this.layerConfigMap.get(layer);
if (!layerInfo) {
return;
}
const { onMouseMove, onMouseOut, onClick, onSourceUpdate } = layerInfo;
if (onMouseMove) {
layer.off('mousemove', onMouseMove);
}
if (onMouseOut) {
layer.off('mouseout', onMouseOut);
}
if (onClick) {
layer.off('click', onClick);
}
if (onSourceUpdate) {
layer?.getSource()?.off('update', onSourceUpdate);
}
});
}
protected onLayerMouseMove(layer: ILayer, e: any) {
if (!this.isSameFeature(layer, e.featureId)) {
const { title, content } = this.getLayerInfoFrag(layer, e);
this.setDOMContent(content);
this.setTitle(title);
this.displayFeatureInfo = {
layer,
featureId: e.featureId,
};
this.show();
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected onLayerMouseOut(layer: ILayer, e: any) {
this.displayFeatureInfo = undefined;
if (this.isShow) {
this.hide();
}
}
protected onLayerClick(layer: ILayer, e: any) {
if (this.isShow && this.isSameFeature(layer, e.featureId)) {
this.hide();
} else {
const { title, content } = this.getLayerInfoFrag(layer, e);
this.setDOMContent(content);
this.setLnglat(e.lngLat);
this.setTitle(title);
this.displayFeatureInfo = {
layer,
featureId: e.featureId,
};
this.show();
}
}
protected onSourceUpdate(layer: ILayer) {
if (this.displayFeatureInfo?.layer === layer) {
this.hide();
this.displayFeatureInfo = undefined;
}
}
/**
* HTML
* @param layer
* @param e
* @protected
*/
protected getLayerInfoFrag(layer: ILayer, e: any) {
const layerInfo = this.layerConfigMap.get(layer);
let titleFrag: DocumentFragment | undefined;
const contentFrag = document.createDocumentFragment();
if (layerInfo) {
let feature = e.feature;
if (
feature.type === 'Feature' &&
'properties' in feature &&
'geometry' in feature
) {
feature = feature.properties;
}
const { title, fields, customContent } = layerInfo;
if (title) {
titleFrag = document.createDocumentFragment();
const titleElement = title instanceof Function ? title(feature) : title;
DOM.appendElementType(titleFrag, titleElement);
}
if (customContent) {
const content =
customContent instanceof Function
? customContent(feature)
: customContent;
DOM.appendElementType(contentFrag, content);
} else if (fields?.length) {
fields?.forEach((fieldConfig) => {
const { field, formatField, formatValue, getValue } =
typeof fieldConfig === 'string'
? // tslint:disable-next-line:no-object-literal-type-assertion
({ field: fieldConfig } as LayerField)
: fieldConfig;
const row = DOM.create('div', 'l7-layer-popup__row');
const value = getValue ? getValue(e.feature) : get(feature, field);
const fieldElement =
(formatField instanceof Function
? formatField(field, feature)
: formatField) ?? field;
const valueElement =
(formatValue instanceof Function
? formatValue(value, feature)
: formatValue) ?? value;
const fieldSpan = DOM.create('span', 'l7-layer-popup__key', row);
DOM.appendElementType(fieldSpan, fieldElement);
DOM.appendElementType(fieldSpan, document.createTextNode(''));
const valueSpan = DOM.create('span', 'l7-layer-popup__value', row);
DOM.appendElementType(valueSpan, valueElement);
contentFrag.appendChild(row);
});
}
}
return {
title: titleFrag,
content: contentFrag,
};
}
/**
* Layer 访 Layer
* @param configItem
* @protected
*/
protected getLayerByConfig(
configItem: LayerPopupConfigItem,
): ILayer | undefined {
const layer = configItem.layer;
if (layer instanceof Object) {
return layer;
}
if (typeof layer === 'string') {
return (
this.layerService.getLayer(layer) ||
this.layerService.getLayerByName(layer)
);
}
}
/**
* Feature
* @param layer
* @param featureId
* @protected
*/
protected isSameFeature(layer: ILayer, featureId: number) {
const displayFeatureInfo = this.displayFeatureInfo;
return (
displayFeatureInfo &&
layer === displayFeatureInfo.layer &&
featureId === displayFeatureInfo.featureId
);
}
}

View File

@ -1,577 +0,0 @@
import {
ILayerService,
ILngLat,
IMapService,
IPopup,
IPopupOption,
ISceneService,
TYPES,
} from '@antv/l7-core';
import {
anchorTranslate,
anchorType,
applyAnchorClass,
DOM,
} from '@antv/l7-utils';
import { EventEmitter } from 'eventemitter3';
import { Container } from 'inversify';
import { createL7Icon } from '../utils/icon';
type ElementType = DOM.ElementType;
export { Popup };
export default class Popup<O extends IPopupOption = IPopupOption>
extends EventEmitter
implements IPopup
{
/**
*
* @protected
*/
protected popupOption: O;
protected mapsService: IMapService;
protected sceneService: ISceneService;
protected layerService: ILayerService;
protected scene: Container;
/**
* DOM
* @protected
*/
protected closeButton?: HTMLElement | SVGElement;
/**
* Popup DOM content tip
* @protected
*/
protected container: HTMLElement;
/**
* popup
* @protected
*/
protected content: HTMLElement;
/**
* popup
* @protected
*/
protected contentTitle?: HTMLElement;
/**
* popup
* @protected
*/
protected contentPanel: HTMLElement;
/**
* DOM
* @protected
*/
protected tip: HTMLElement;
/**
*
* @protected
*/
protected isShow: boolean = true;
protected get lngLat() {
return (
this.popupOption.lngLat ?? {
lng: 0,
lat: 0,
}
);
}
protected set lngLat(newLngLat: ILngLat) {
this.popupOption.lngLat = newLngLat;
}
constructor(cfg?: Partial<O>) {
super();
this.popupOption = {
...this.getDefault(cfg ?? {}),
...cfg,
};
const { lngLat } = this.popupOption;
if (lngLat) {
this.lngLat = lngLat;
}
}
public getIsShow() {
return this.isShow;
}
public addTo(scene: Container) {
this.mapsService = scene.get<IMapService>(TYPES.IMapService);
this.sceneService = scene.get<ISceneService>(TYPES.ISceneService);
this.layerService = scene.get<ILayerService>(TYPES.ILayerService);
this.mapsService.on('camerachange', this.update);
this.mapsService.on('viewchange', this.update);
this.scene = scene;
this.update();
// 临时关闭
this.updateCloseOnClick();
this.updateCloseOnEsc();
this.updateFollowCursor();
const { html, text, title } = this.popupOption;
if (html) {
this.setHTML(html);
} else if (text) {
this.setText(text);
}
if (title) {
this.setTitle(title);
}
this.emit('open');
return this;
}
// 移除popup
public remove() {
if (!this.isOpen()) {
return;
}
if (this.content) {
DOM.remove(this.content);
}
if (this.container) {
DOM.remove(this.container);
// @ts-ignore
delete this.container;
}
if (this.mapsService) {
// TODO: mapbox AMap 事件同步
this.mapsService.off('camerachange', this.update);
this.mapsService.off('viewchange', this.update);
this.updateCloseOnClick(true);
this.updateCloseOnEsc(true);
this.updateFollowCursor(true);
// @ts-ignore
delete this.mapsService;
}
this.emit('close');
return this;
}
/**
* option
*/
public getOptions() {
return this.popupOption;
}
public setOptions(option: Partial<O>) {
this.show();
this.popupOption = {
...this.popupOption,
...option,
};
if (
this.checkUpdateOption(option, [
'html',
'text',
'title',
'closeButton',
'closeButtonOffsets',
'maxWidth',
'anchor',
'stopPropagation',
'className',
'style',
'lngLat',
'offsets',
])
) {
if (this.container) {
DOM.remove(this.container);
// @ts-ignore
this.container = undefined;
}
if (this.popupOption.title) {
this.setTitle(this.popupOption.title);
}
if (this.popupOption.html) {
this.setHTML(this.popupOption.html);
} else if (this.popupOption.text) {
this.setText(this.popupOption.text);
}
}
if (this.checkUpdateOption(option, ['closeOnEsc'])) {
this.updateCloseOnEsc();
}
if (this.checkUpdateOption(option, ['closeOnClick'])) {
this.updateCloseOnClick();
}
if (this.checkUpdateOption(option, ['followCursor'])) {
this.updateFollowCursor();
}
if (this.checkUpdateOption(option, ['html']) && option.html) {
this.setHTML(option.html);
} else if (this.checkUpdateOption(option, ['text']) && option.text) {
this.setText(option.text);
}
if (this.checkUpdateOption(option, ['lngLat']) && option.lngLat) {
this.setLnglat(option.lngLat);
}
return this;
}
public open() {
this.addTo(this.scene);
return this;
}
public close() {
this.remove();
return this;
}
public show() {
if (this.isShow) {
return;
}
if (this.container) {
DOM.removeClass(this.container, 'l7-popup-hide');
}
this.isShow = true;
this.emit('show');
return this;
}
public hide() {
if (!this.isShow) {
return;
}
if (this.container) {
DOM.addClass(this.container, 'l7-popup-hide');
}
this.isShow = false;
this.emit('hide');
return this;
}
/**
* HTML
* @param html
*/
public setHTML(html: ElementType) {
this.popupOption.html = html;
return this.setDOMContent(html);
}
/**
* Popup
* @param text
*/
public setText(text: string) {
this.popupOption.text = text;
return this.setDOMContent(window.document.createTextNode(text));
}
public setTitle(title?: ElementType) {
this.show();
this.popupOption.title = title;
if (title) {
if (!this.contentTitle) {
this.contentTitle = DOM.create('div', 'l7-popup-content__title');
if (this.content.firstChild) {
this.content.insertBefore(
this.contentTitle!,
this.content.firstChild,
);
} else {
this.content.append(this.contentTitle!);
}
}
DOM.clearChildren(this.contentTitle!);
DOM.appendElementType(this.contentTitle!, title);
} else if (this.contentTitle) {
DOM.remove(this.contentTitle);
this.contentTitle = undefined;
}
}
/**
*
*/
public panToPopup() {
const { lng, lat } = this.lngLat;
if (this.popupOption.autoPan) {
this.mapsService.panTo([lng, lat]);
}
return this;
}
public setLngLat(lngLat: ILngLat | [number, number]): this {
return this.setLnglat(lngLat);
}
/**
* Popup
* @param lngLat
*/
public setLnglat(lngLat: ILngLat | [number, number]): this {
this.show();
this.lngLat = lngLat as ILngLat;
if (Array.isArray(lngLat)) {
this.lngLat = {
lng: lngLat[0],
lat: lngLat[1],
};
}
if (this.mapsService) {
// 防止事件重复监听
this.mapsService.off('camerachange', this.update);
this.mapsService.off('viewchange', this.update);
this.mapsService.on('camerachange', this.update);
this.mapsService.on('viewchange', this.update);
}
this.update();
if (this.popupOption.autoPan) {
setTimeout(() => {
this.panToPopup();
}, 0);
}
return this;
}
/**
* Popup
*/
public getLnglat(): ILngLat {
return this.lngLat;
}
/**
* Popup
* @param maxWidth
*/
public setMaxWidth(maxWidth: string): this {
this.popupOption.maxWidth = maxWidth;
this.update();
return this;
}
public isOpen() {
return !!this.mapsService;
}
protected onMouseMove = (e: MouseEvent) => {
const container = this.mapsService.getMapContainer();
const { left = 0, top = 0 } = container?.getBoundingClientRect() ?? {};
this.setPopupPosition(e.clientX - left, e.clientY - top);
};
/**
*
* @protected
*/
protected updateLngLatPosition = () => {
if (!this.mapsService || this.popupOption.followCursor) {
return;
}
const { lng, lat } = this.lngLat;
const { x, y } = this.mapsService.lngLatToContainer([lng, lat]);
this.setPopupPosition(x, y);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getDefault(option: Partial<O>): O {
// tslint:disable-next-line:no-object-literal-type-assertion
return {
closeButton: true,
closeOnClick: false,
maxWidth: '240px',
offsets: [0, 0],
anchor: anchorType.BOTTOM,
stopPropagation: true,
autoPan: false,
autoClose: true,
closeOnEsc: false,
followCursor: false,
} as O;
}
/**
* Popup HTML
* @param element
*/
protected setDOMContent(element: ElementType) {
this.show();
this.createContent();
DOM.appendElementType(this.contentPanel, element);
this.update();
return this;
}
/**
* Popup
* @protected
*/
protected updateCloseOnClick(onlyClear?: boolean) {
const mapsService = this.mapsService;
if (mapsService) {
mapsService?.off('click', this.onCloseButtonClick);
if (this.popupOption.closeOnClick && !onlyClear) {
requestAnimationFrame(() => {
mapsService?.on('click', this.onCloseButtonClick);
});
}
}
}
protected updateCloseOnEsc(onlyClear?: boolean) {
window.removeEventListener('keydown', this.onKeyDown);
if (this.popupOption.closeOnEsc && !onlyClear) {
window.addEventListener('keydown', this.onKeyDown);
}
}
protected updateFollowCursor(onlyClear?: boolean) {
const container = this.mapsService?.getContainer();
if (container) {
container?.removeEventListener('mousemove', this.onMouseMove);
if (this.popupOption.followCursor && !onlyClear) {
container?.addEventListener('mousemove', this.onMouseMove);
}
}
}
protected onKeyDown = (e: KeyboardEvent) => {
if (e.keyCode === 27) {
this.remove();
}
};
/**
* Popup DOM setHTML setText
* @protected
*/
protected createContent() {
if (this.content) {
DOM.remove(this.content);
}
this.contentTitle = undefined;
this.content = DOM.create('div', 'l7-popup-content', this.container);
this.setTitle(this.popupOption.title);
if (this.popupOption.closeButton) {
const closeButton = createL7Icon('l7-icon-guanbi');
DOM.addClass(closeButton, 'l7-popup-close-button');
this.content.appendChild(closeButton);
if (this.popupOption.closeButtonOffsets) {
// 关闭按钮的偏移
closeButton.style.right = this.popupOption.closeButtonOffsets[0] + 'px';
closeButton.style.top = this.popupOption.closeButtonOffsets[1] + 'px';
}
// this.closeButton.type = 'button';
closeButton.setAttribute('aria-label', 'Close popup');
closeButton.addEventListener('click', () => {
this.hide();
});
this.closeButton = closeButton;
} else {
this.closeButton = undefined;
}
this.contentPanel = DOM.create(
'div',
'l7-popup-content__panel',
this.content,
);
}
protected onCloseButtonClick = (e: Event) => {
if (e.stopPropagation) {
e.stopPropagation();
}
this.hide();
};
protected update = () => {
const hasPosition = !!this.lngLat;
const { className, style, maxWidth, anchor, stopPropagation } =
this.popupOption;
if (!this.mapsService || !hasPosition || !this.content) {
return;
}
const popupContainer = this.mapsService.getMarkerContainer();
// 如果当前没有创建 Popup 容器则创建
if (!this.container && popupContainer) {
this.container = DOM.create(
'div',
`l7-popup ${className ?? ''} ${!this.isShow ? 'l7-popup-hide' : ''}`,
popupContainer as HTMLElement,
);
if (style) {
this.container.setAttribute('style', style);
}
this.tip = DOM.create('div', 'l7-popup-tip', this.container);
this.container.appendChild(this.content);
// 高德地图需要阻止事件冒泡 // 测试mapbox 地图不需要添加
if (stopPropagation) {
['mousemove', 'mousedown', 'mouseup', 'click', 'dblclick'].forEach(
(type) => {
this.container.addEventListener(type, (e) => {
e.stopPropagation();
});
},
);
}
this.container.style.whiteSpace = 'nowrap';
}
this.updateLngLatPosition();
DOM.setTransform(this.container, `${anchorTranslate[anchor]}`);
applyAnchorClass(this.container, anchor, 'popup');
if (maxWidth) {
const { width } = this.container.getBoundingClientRect();
if (width > parseFloat(maxWidth)) {
this.container.style.width = maxWidth;
}
} else {
this.container.style.removeProperty('width');
}
};
/**
* Popup Position
* @param left
* @param top
* @protected
*/
protected setPopupPosition(left: number, top: number) {
if (this.container) {
const { offsets } = this.popupOption;
this.container.style.left = left + offsets[0] + 'px';
this.container.style.top = top - offsets[1] + 'px';
}
}
/**
* option keys
* @param option
* @param keys
* @protected
*/
protected checkUpdateOption(option: Partial<O>, keys: Array<keyof O>) {
return keys.some((key) => key in option);
}
}

View File

@ -1,36 +0,0 @@
export enum anchorType {
'CENTER' = 'center',
'TOP' = 'top',
'TOP-LEFT' = 'top-left',
'TOP-RIGHT' = 'top-right',
'BOTTOM' = 'bottom',
'BOTTOM-LEFT' = 'bottom-left',
'LEFT' = 'left',
'RIGHT' = 'right',
}
export const anchorTranslate = {
center: 'translate(-50%,-50%)',
top: 'translate(-50%,0)',
'top-left': 'translate(0,0)',
'top-right': 'translate(-100%,0)',
bottom: 'translate(-50%,-100%)',
'bottom-left': 'translate(0,-100%)',
'bottom-right': 'translate(-100%,-100%)',
left: 'translate(0,-50%)',
right: 'translate(-100%,-50%)',
};
export function applyAnchorClass(
element: HTMLElement,
anchor: string,
prefix: string,
) {
const classList = element.classList;
for (const key in anchorTranslate) {
if (anchorTranslate.hasOwnProperty(key)) {
classList.remove(`l7-${prefix}-anchor-${key}`);
}
}
classList.add(`l7-${prefix}-anchor-${anchor}`);
}

View File

@ -1,9 +0,0 @@
export const createL7Icon = (className: string) => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('l7-iconfont');
svg.setAttribute('aria-hidden', 'true');
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', `#${className}`);
svg.appendChild(use);
return svg;
};

View File

@ -1,286 +0,0 @@
import { DOM } from '@antv/l7-utils';
import { EventEmitter } from 'eventemitter3';
/**
*
*/
export type PopperPlacement =
| 'top-start'
| 'top'
| 'top-end'
| 'left-start'
| 'left'
| 'left-end'
| 'bottom-start'
| 'bottom'
| 'bottom-end'
| 'right-start'
| 'right'
| 'right-end';
/**
* click hover
*/
export type PopperTrigger = 'click' | 'hover';
/**
*
*/
export type PopperContent = string | HTMLElement | null;
export interface IPopperOption {
placement: PopperPlacement; // 气泡展示方向
trigger: PopperTrigger; // 气泡触发方式
content?: PopperContent; // 初始内容
offset?: [number, number]; // 气泡偏移
className?: string; // 容器自定义 className
container: HTMLElement; // 触发气泡的容器
unique?: boolean; // 当前气泡展示时,是否关闭其他该配置为 true 的气泡
}
export class Popper extends EventEmitter<'show' | 'hide'> {
protected get buttonRect() {
return this.button.getBoundingClientRect();
}
protected static conflictPopperList: Popper[] = [];
// 气泡容器 DOM
public popperDOM!: HTMLElement;
// 气泡中展示的内容容器 DOM
public contentDOM!: HTMLElement;
/**
*
* @protected
*/
protected button: HTMLElement;
/**
* Popper
* @protected
*/
protected option: IPopperOption;
/**
*
* @protected
*/
protected isShow: boolean = false;
/**
*
* @protected
*/
protected content: PopperContent;
/**
*
* @protected
*/
protected timeout: number | null = null;
constructor(button: HTMLElement, option: IPopperOption) {
super();
this.button = button;
this.option = option;
this.init();
if (option.unique) {
Popper.conflictPopperList.push(this);
}
}
public getPopperDOM() {
return this.popperDOM;
}
public getIsShow() {
return this.isShow;
}
public getContent() {
return this.content;
}
public setContent(content: PopperContent) {
if (typeof content === 'string') {
this.contentDOM.innerHTML = content;
} else if (content instanceof HTMLElement) {
DOM.clearChildren(this.contentDOM);
this.contentDOM.appendChild(content);
}
this.content = content;
}
public show = () => {
if (this.isShow || !this.contentDOM.innerHTML) {
return this;
}
this.resetPopperPosition();
DOM.removeClass(this.popperDOM, 'l7-popper-hide');
this.isShow = true;
if (this.option.unique) {
// console.log(Popper.conflictPopperList.length);
Popper.conflictPopperList.forEach((popper) => {
if (popper !== this && popper.isShow) {
popper.hide();
}
});
}
this.emit('show');
return this;
};
public hide = () => {
if (!this.isShow) {
return this;
}
DOM.addClass(this.popperDOM, 'l7-popper-hide');
this.isShow = false;
this.emit('hide');
return this;
};
/**
*
*/
public setHideTimeout = () => {
if (this.timeout) {
return;
}
this.timeout = window.setTimeout(() => {
if (!this.isShow) {
return;
}
this.hide();
this.timeout = null;
}, 300);
};
/**
*
*/
public clearHideTimeout = () => {
if (this.timeout) {
window.clearTimeout(this.timeout);
this.timeout = null;
}
};
public init() {
const { trigger } = this.option;
this.popperDOM = this.createPopper();
if (trigger === 'click') {
this.button.addEventListener('click', this.onBtnClick);
} else {
this.button.addEventListener('mousemove', this.onBtnMouseMove);
this.button.addEventListener('mouseleave', this.onBtnMouseLeave);
this.popperDOM.addEventListener('mousemove', this.onBtnMouseMove);
this.popperDOM.addEventListener('mouseleave', this.onBtnMouseLeave);
}
}
public destroy() {
this.button.removeEventListener('click', this.onBtnClick);
this.button.removeEventListener('mousemove', this.onBtnMouseMove);
this.button.removeEventListener('mousemove', this.onBtnMouseLeave);
this.popperDOM.removeEventListener('mousemove', this.onBtnMouseMove);
this.popperDOM.removeEventListener('mouseleave', this.onBtnMouseLeave);
DOM.remove(this.popperDOM);
}
public resetPopperPosition() {
const popperStyleObj: any = {};
const { container, offset = [0, 0], placement } = this.option;
const [offsetX, offsetY] = offset;
const buttonRect = this.button.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const { left, right, top, bottom } = DOM.getDiffRect(
buttonRect,
containerRect,
);
let isTransformX = false;
let isTransformY = false;
if (/^(left|right)/.test(placement)) {
if (placement.includes('left')) {
popperStyleObj.right = `${buttonRect.width + right}px`;
} else if (placement.includes('right')) {
popperStyleObj.left = `${buttonRect.width + left}px`;
}
if (placement.includes('start')) {
popperStyleObj.top = `${top}px`;
} else if (placement.includes('end')) {
popperStyleObj.bottom = `${bottom}px`;
} else {
popperStyleObj.top = `${top + buttonRect.height / 2}px`;
isTransformY = true;
popperStyleObj.transform = `translate(${offsetX}px, calc(${offsetY}px - 50%))`;
}
} else if (/^(top|bottom)/.test(placement)) {
if (placement.includes('top')) {
popperStyleObj.bottom = `${buttonRect.height + bottom}px`;
} else if (placement.includes('bottom')) {
popperStyleObj.top = `${buttonRect.height + top}px`;
}
if (placement.includes('start')) {
popperStyleObj.left = `${left}px`;
} else if (placement.includes('end')) {
popperStyleObj.right = `${right}px`;
} else {
popperStyleObj.left = `${left + buttonRect.width / 2}px`;
isTransformX = true;
popperStyleObj.transform = `translate(calc(${offsetX}px - 50%), ${offsetY}px)`;
}
}
popperStyleObj.transform = `translate(calc(${offsetX}px - ${
isTransformX ? '50%' : '0%'
}), calc(${offsetY}px - ${isTransformY ? '50%' : '0%'})`;
const posList = placement.split('-');
if (posList.length) {
DOM.addClass(
this.popperDOM,
posList.map((pos) => `l7-popper-${pos}`).join(' '),
);
}
DOM.addStyle(this.popperDOM, DOM.css2Style(popperStyleObj));
}
protected createPopper(): HTMLElement {
const { container, className = '', content } = this.option;
const popper = DOM.create(
'div',
`l7-popper l7-popper-hide ${className}`,
) as HTMLElement;
const popperContent = DOM.create('div', 'l7-popper-content') as HTMLElement;
const popperArrow = DOM.create('div', 'l7-popper-arrow') as HTMLElement;
popper.appendChild(popperContent);
popper.appendChild(popperArrow);
container.appendChild(popper);
this.popperDOM = popper;
this.contentDOM = popperContent;
if (content) {
this.setContent(content);
}
return popper;
}
protected onBtnClick = () => {
if (this.isShow) {
this.hide();
} else {
this.show();
}
};
protected onBtnMouseLeave = () => {
this.setHideTimeout();
};
protected onBtnMouseMove = () => {
this.clearHideTimeout();
if (this.isShow) {
return;
}
this.show();
};
}

View File

@ -1,159 +0,0 @@
// @ts-nocheck
const methodMap = [
[
'requestFullscreen',
'exitFullscreen',
'fullscreenElement',
'fullscreenEnabled',
'fullscreenchange',
'fullscreenerror',
],
// New WebKit
[
'webkitRequestFullscreen',
'webkitExitFullscreen',
'webkitFullscreenElement',
'webkitFullscreenEnabled',
'webkitfullscreenchange',
'webkitfullscreenerror',
],
// Old WebKit
[
'webkitRequestFullScreen',
'webkitCancelFullScreen',
'webkitCurrentFullScreenElement',
'webkitCancelFullScreen',
'webkitfullscreenchange',
'webkitfullscreenerror',
],
[
'mozRequestFullScreen',
'mozCancelFullScreen',
'mozFullScreenElement',
'mozFullScreenEnabled',
'mozfullscreenchange',
'mozfullscreenerror',
],
[
'msRequestFullscreen',
'msExitFullscreen',
'msFullscreenElement',
'msFullscreenEnabled',
'MSFullscreenChange',
'MSFullscreenError',
],
];
const nativeAPI = (() => {
if (typeof document === 'undefined') {
return false;
}
const unprefixedMethods = methodMap[0];
const returnValue = {};
for (const methodList of methodMap) {
const exitFullscreenMethod = methodList?.[1];
if (exitFullscreenMethod in document) {
for (const [index, method] of methodList.entries()) {
returnValue[unprefixedMethods[index]] = method;
}
return returnValue;
}
}
return false;
})();
const eventNameMap = {
change: nativeAPI.fullscreenchange,
error: nativeAPI.fullscreenerror,
};
let screenfull: any = {
// eslint-disable-next-line default-param-last
request(element = document.documentElement, options) {
return new Promise((resolve, reject) => {
const onFullScreenEntered = () => {
screenfull.off('change', onFullScreenEntered);
resolve();
};
screenfull.on('change', onFullScreenEntered);
const returnPromise = element[nativeAPI.requestFullscreen](options);
if (returnPromise instanceof Promise) {
returnPromise.then(onFullScreenEntered).catch(reject);
}
});
},
exit() {
return new Promise((resolve, reject) => {
if (!screenfull.isFullscreen) {
resolve();
return;
}
const onFullScreenExit = () => {
screenfull.off('change', onFullScreenExit);
resolve();
};
screenfull.on('change', onFullScreenExit);
const returnPromise = document[nativeAPI.exitFullscreen]();
if (returnPromise instanceof Promise) {
returnPromise.then(onFullScreenExit).catch(reject);
}
});
},
toggle(element, options) {
return screenfull.isFullscreen
? screenfull.exit()
: screenfull.request(element, options);
},
onchange(callback) {
screenfull.on('change', callback);
},
onerror(callback) {
screenfull.on('error', callback);
},
on(event, callback) {
const eventName = eventNameMap[event];
if (eventName) {
document.addEventListener(eventName, callback, false);
}
},
off(event, callback) {
const eventName = eventNameMap[event];
if (eventName) {
document.removeEventListener(eventName, callback, false);
}
},
raw: nativeAPI,
};
Object.defineProperties(screenfull, {
isFullscreen: {
get: () => Boolean(document[nativeAPI.fullscreenElement]),
},
element: {
enumerable: true,
get: () => document[nativeAPI.fullscreenElement] ?? undefined,
},
isEnabled: {
enumerable: true,
// Coerce to boolean in case of old WebKit.
get: () => Boolean(document[nativeAPI.fullscreenEnabled]),
},
});
if (!nativeAPI) {
screenfull = { isEnabled: false };
}
export default screenfull;

View File

@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"declarationDir": "./es",
"rootDir": "./src",
"baseUrl": "./"
},
"include": ["./src"]
}

View File

@ -54,7 +54,7 @@ export * from './services/renderer/IBuffer';
export * from './services/renderer/IElements';
export * from './services/renderer/IFramebuffer';
export * from './services/renderer/IModel';
export * from './services/renderer/IMultiPassRenderer';
export * from './services/renderer/IRenderbuffer';
export * from './services/renderer/ITexture2D';
export * from './services/renderer/IUniform';

View File

@ -19,7 +19,7 @@ import { ICoordinateSystemService } from './services/coordinate/ICoordinateSyste
import { IInteractionService } from './services/interaction/IInteractionService';
import { IPickingService } from './services/interaction/IPickingService';
import { ILayerService } from './services/layer/ILayerService';
import { IStyleAttributeService } from './services/layer/IStyleAttributeService';
import { ISceneService } from './services/scene/ISceneService';
import { IShaderModuleService } from './services/shader/IShaderModuleService';
@ -35,34 +35,21 @@ import CoordinateSystemService from './services/coordinate/CoordinateSystemServi
import InteractionService from './services/interaction/InteractionService';
import PickingService from './services/interaction/PickingService';
import LayerService from './services/layer/LayerService';
import StyleAttributeService from './services/layer/StyleAttributeService';
import SceneService from './services/scene/SceneService';
import ShaderModuleService from './services/shader/ShaderModuleService';
/** PostProcessing passes */
import { IMarkerService } from './services/component/IMarkerService';
import { IPopupService } from './services/component/IPopupService';
import {
IMultiPassRenderer,
IPass,
IPostProcessingPass,
IPostProcessor,
} from './services/renderer/IMultiPassRenderer';
import ClearPass from './services/renderer/passes/ClearPass';
import MultiPassRenderer from './services/renderer/passes/MultiPassRenderer';
import PixelPickingPass from './services/renderer/passes/PixelPickingPass';
import BloomPass from './services/renderer/passes/post-processing/BloomPass';
import BlurHPass from './services/renderer/passes/post-processing/BlurHPass';
import BlurVPass from './services/renderer/passes/post-processing/BlurVPass';
import ColorHalfTonePass from './services/renderer/passes/post-processing/ColorHalfTonePass';
import CopyPass from './services/renderer/passes/post-processing/CopyPass';
import HexagonalPixelatePass from './services/renderer/passes/post-processing/HexagonalPixelatePass';
import InkPass from './services/renderer/passes/post-processing/InkPass';
import NoisePass from './services/renderer/passes/post-processing/NoisePass';
import SepiaPass from './services/renderer/passes/post-processing/SepiaPass';
import PostProcessor from './services/renderer/passes/PostProcessor';
import RenderPass from './services/renderer/passes/RenderPass';
import TAAPass from './services/renderer/passes/TAAPass';
// @see https://github.com/inversify/InversifyJS/blob/master/wiki/container_api.md#defaultscope
const container = new Container();
@ -200,18 +187,12 @@ export function createSceneContainer() {
.bind<IPass<unknown>>(TYPES.INormalPass)
.to(ClearPass)
.whenTargetNamed('clear');
sceneContainer
.bind<IPass<unknown>>(TYPES.INormalPass)
.to(PixelPickingPass)
.whenTargetNamed('pixelPicking');
sceneContainer
.bind<IPass<unknown>>(TYPES.INormalPass)
.to(RenderPass)
.whenTargetNamed('render');
sceneContainer
.bind<IPass<unknown>>(TYPES.INormalPass)
.to(TAAPass)
.whenTargetNamed('taa');
sceneContainer
.bind<interfaces.Factory<IPass<unknown>>>(TYPES.IFactoryNormalPass)
.toFactory<IPass<unknown>>((context) => (named: string) =>
@ -222,39 +203,7 @@ export function createSceneContainer() {
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(CopyPass)
.whenTargetNamed('copy');
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(BloomPass)
.whenTargetNamed('bloom');
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(BlurHPass)
.whenTargetNamed('blurH');
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(BlurVPass)
.whenTargetNamed('blurV');
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(NoisePass)
.whenTargetNamed('noise');
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(SepiaPass)
.whenTargetNamed('sepia');
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(ColorHalfTonePass)
.whenTargetNamed('colorHalftone');
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(HexagonalPixelatePass)
.whenTargetNamed('hexagonalPixelate');
sceneContainer
.bind<IPostProcessingPass<unknown>>(TYPES.IPostProcessingPass)
.to(InkPass)
.whenTargetNamed('ink');
// 绑定工厂方法
sceneContainer
@ -277,17 +226,6 @@ export function createLayerContainer(sceneContainer: Container) {
const layerContainer = new Container();
layerContainer.parent = sceneContainer;
layerContainer
.bind<IStyleAttributeService>(TYPES.IStyleAttributeService)
.to(StyleAttributeService)
.inSingletonScope();
layerContainer
.bind<IMultiPassRenderer>(TYPES.IMultiPassRenderer)
.to(MultiPassRenderer)
.inSingletonScope();
layerContainer
.bind<IPostProcessor>(TYPES.IPostProcessor)
.to(PostProcessor)
.inSingletonScope();
return layerContainer;
}

View File

@ -64,7 +64,7 @@ const defaultLayerConfig: Partial<ILayerConfig> = {
zIndex: 0,
blend: 'normal',
pickedFeatureID: -1,
enableMultiPassRenderer: false,
enablePicking: true,
active: false,
activeColor: '#2f54eb',

Some files were not shown because too many files have changed in this diff Show More