diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ec38942f22..02422c05d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -39,6 +39,7 @@ export { }; /** 暴露服务接口供其他 packages 实现 */ +export * from './services/layer/ILayerStyleService'; export * from './services/layer/ILayerService'; export * from './services/source/ISourceService'; export * from './services/map/IMapService'; @@ -48,6 +49,7 @@ export * from './services/camera/ICameraService'; export * from './services/config/IConfigService'; export * from './services/scene/ISceneService'; export * from './services/shader/IShaderModuleService'; +export * from './services/asset/IIconService'; /** 全部渲染服务接口 */ export * from './services/renderer/IAttribute'; diff --git a/packages/core/src/inversify.config.ts b/packages/core/src/inversify.config.ts index 4ddb94071b..30e69bcc00 100644 --- a/packages/core/src/inversify.config.ts +++ b/packages/core/src/inversify.config.ts @@ -6,6 +6,7 @@ import getDecorators from 'inversify-inject-decorators'; import { TYPES } from './types'; /** Service interfaces */ +import { IIconService } from './services/asset/IIconService'; import { ICameraService } from './services/camera/ICameraService'; import { IGlobalConfigService } from './services/config/IConfigService'; import { ICoordinateSystemService } from './services/coordinate/ICoordinateSystemService'; @@ -15,6 +16,7 @@ import { ILogService } from './services/log/ILogService'; import { IShaderModuleService } from './services/shader/IShaderModuleService'; /** Service implements */ +import IconService from './services/asset/IconService'; import CameraService from './services/camera/CameraService'; import GlobalConfigService from './services/config/ConfigService'; import CoordinateSystemService from './services/coordinate/CoordinateSystemService'; @@ -46,6 +48,10 @@ container .bind(TYPES.ICoordinateSystemService) .to(CoordinateSystemService) .inSingletonScope(); +container + .bind(TYPES.IIconService) + .to(IconService) + .inSingletonScope(); container .bind(TYPES.IShaderModuleService) .to(ShaderModuleService) diff --git a/packages/core/src/services/asset/IIconService.ts b/packages/core/src/services/asset/IIconService.ts new file mode 100644 index 0000000000..a673e13c8b --- /dev/null +++ b/packages/core/src/services/asset/IIconService.ts @@ -0,0 +1,24 @@ +import { ITexture2D } from '../renderer/ITexture2D'; +export type IImage = HTMLImageElement | File | string; +export interface IIconValue { + x: number; + y: number; + image: HTMLImageElement; +} +export interface IIcon { + id: string; + image: HTMLImageElement; + height: number; + width: number; +} +export interface IICONMap { + [key: string]: IIconValue; +} +export interface IIconService { + canvasHeight: number; + init(): void; + addImage(id: string, image: IImage): void; + getTexture(): ITexture2D; + getIconMap(): IICONMap; + getCanvas(): HTMLCanvasElement; +} diff --git a/packages/core/src/services/asset/IconService.ts b/packages/core/src/services/asset/IconService.ts new file mode 100644 index 0000000000..68546ea1eb --- /dev/null +++ b/packages/core/src/services/asset/IconService.ts @@ -0,0 +1,97 @@ +import { inject, injectable } from 'inversify'; +import { TYPES } from '../../types'; +import { buildIconMaping } from '../../utils/font_util'; +import { gl } from '../renderer/gl'; +import { IRendererService } from '../renderer/IRendererService'; +import { ITexture2D } from '../renderer/ITexture2D'; +import { + IIcon, + IICONMap, + IIconService, + IIconValue, + IImage, +} from './IIconService'; +const BUFFER = 3; +const MAX_CANVAS_WIDTH = 1024; +const imageSize = 64; +@injectable() +export default class IconService implements IIconService { + public canvasHeight: number; + private textrure: ITexture2D; + private canvas: HTMLCanvasElement; + private iconData: IIcon[]; + private iconMap: IICONMap; + private ctx: CanvasRenderingContext2D; + public init() { + this.iconData = []; + this.iconMap = {}; + this.canvas = document.createElement('canvas'); + this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D; + } + + public addImage(id: string, image: IImage) { + let imagedata = new Image(); + this.loadImage(image).then((img) => { + imagedata = img as HTMLImageElement; + }); + this.iconData.push({ + id, + image: imagedata, + width: imageSize, + height: imageSize, + }); + const { mapping, canvasHeight } = buildIconMaping( + this.iconData, + BUFFER, + MAX_CANVAS_WIDTH, + ); + this.iconMap = mapping; + this.canvasHeight = canvasHeight; + this.updateIconAtlas(); + } + + public getTexture(): ITexture2D { + return this.textrure; + } + + public getIconMap() { + return this.iconMap; + } + public getCanvas() { + return this.canvas; + } + + private updateIconAtlas() { + this.canvas.width = MAX_CANVAS_WIDTH; + this.canvas.height = this.canvasHeight; + Object.keys(this.iconMap).forEach((item: string) => { + const { x, y, image } = this.iconMap[item]; + this.ctx.drawImage(image, x, y, imageSize, imageSize); + }); + // const { createTexture2D } = this.rendererService; + // this.textrure = createTexture2D({ + // data: this.canvas, + // width: this.canvas.width, + // height: this.canvasHeight, + // mag: gl.LINEAR, + // }); + } + + private loadImage(url: IImage) { + return new Promise((resolve, reject) => { + if (url instanceof HTMLImageElement) { + resolve(url); + return; + } + const image = new Image(); + image.crossOrigin = 'anonymous'; + image.onload = () => { + resolve(image); + }; + image.onerror = () => { + reject(new Error('Could not load image at ' + url)); + }; + image.src = url instanceof File ? URL.createObjectURL(url) : url; + }); + } +} diff --git a/packages/core/src/services/camera/ICameraService.ts b/packages/core/src/services/camera/ICameraService.ts index 5d7dcfdb1b..392bca7d2c 100644 --- a/packages/core/src/services/camera/ICameraService.ts +++ b/packages/core/src/services/camera/ICameraService.ts @@ -8,6 +8,7 @@ export const CameraUniform = { Zoom: 'u_Zoom', ZoomScale: 'u_ZoomScale', FocalDistance: 'u_FocalDistance', + CameraPosition: 'u_CameraPosition', }; export interface IViewport { diff --git a/packages/core/src/services/layer/ILayerService.ts b/packages/core/src/services/layer/ILayerService.ts index 9b079c9117..7f60a8873e 100644 --- a/packages/core/src/services/layer/ILayerService.ts +++ b/packages/core/src/services/layer/ILayerService.ts @@ -1,9 +1,9 @@ -import { ISourceCFG } from '@l7/source'; +import { ISourceCFG } from '@l7/core'; import { AsyncParallelHook, SyncHook } from 'tapable'; import { IModel } from '../renderer/IModel'; import { IMultiPassRenderer } from '../renderer/IMultiPassRenderer'; import { ISource } from '../source/ISourceService'; - +import { ILayerStyleOptions } from './ILayerStyleService'; export enum ScaleTypes { LINEAR = 'linear', POWER = 'power', @@ -15,8 +15,11 @@ export enum ScaleTypes { THRESHOLD = 'threshold', CAT = 'cat', } - -export interface IScale { +export enum StyleScaleType { + CONSTANT = 'constant', + VARIABLE = 'variable', +} +export interface IScaleOption { field?: string; type: ScaleTypes; ticks?: any[]; @@ -24,23 +27,32 @@ export interface IScale { format?: () => any; domain?: any[]; } +export interface IStyleScale { + scale: any; + field: string; + type: StyleScaleType; + option: IScaleOption; +} export interface ILayerGlobalConfig { colors: string[]; size: number; shape: string; scales: { - [key: string]: IScale; + [key: string]: IScaleOption; }; } - +type CallBack = (...args: any[]) => any; export type StyleAttributeField = string | string[]; +export type StyleAttributeOption = string | number | boolean | any[] | CallBack; +export type StyleAttrField = string | string[] | number | number[]; export interface ILayerStyleAttribute { - type?: string; - names?: string[]; - field?: StyleAttributeField; + type: string; + names: string[]; + field: StyleAttributeField; values?: any[]; - scales?: any[]; + scales?: IStyleScale[]; + setScales: (scales: IStyleScale[]) => void; callback?: (...args: any[]) => []; mapping?(...params: unknown[]): unknown[]; } @@ -61,15 +73,19 @@ export interface ILayer { styleAttributes: { [attributeName: string]: ILayerStyleAttribute; }; + sourceOption: { + data: any; + options?: ISourceCFG; + }; multiPassRenderer: IMultiPassRenderer; init(): ILayer; - // size(field: string, value: AttrOption): ILayer; - // color(field: string, value: AttrOption): ILayer; - // shape(field: string, value: AttrOption): ILayer; - // pattern(field: string, value: AttrOption): ILayer; - // filter(field: string, value: AttrOption): ILayer; + size(field: StyleAttrField, value?: StyleAttributeOption): ILayer; + color(field: StyleAttrField, value?: StyleAttributeOption): ILayer; + shape(field: StyleAttrField, value?: StyleAttributeOption): ILayer; + // pattern(field: string, value: StyleAttributeOption): ILayer; + // filter(field: string, value: StyleAttributeOption): ILayer; // active(option: ActiveOption): ILayer; - // style(options: ILayerStyleOptions): ILayer; + style(options: ILayerStyleOptions): ILayer; // hide(): ILayer; // show(): ILayer; // animate(field: string, option: any): ILayer; @@ -78,6 +94,7 @@ export interface ILayer { source(data: any, option?: ISourceCFG): ILayer; addPlugin(plugin: ILayerPlugin): ILayer; getSource(): ISource; + setSource(source: ISource): void; setEncodedData(encodedData: Array<{ [key: string]: unknown }>): void; getEncodedData(): Array<{ [key: string]: unknown }>; getInitializationOptions(): Partial; diff --git a/packages/core/src/services/renderer/ITexture2D.ts b/packages/core/src/services/renderer/ITexture2D.ts index 9fd28ca068..31295eafa9 100644 --- a/packages/core/src/services/renderer/ITexture2D.ts +++ b/packages/core/src/services/renderer/ITexture2D.ts @@ -33,6 +33,8 @@ export interface ITexture2DInitializationOptions { */ data?: | undefined + | HTMLCanvasElement + | HTMLImageElement | number[] | number[][] | Uint8Array diff --git a/packages/core/src/services/scene/ISceneService.ts b/packages/core/src/services/scene/ISceneService.ts index cd9c7a664a..c84050d616 100644 --- a/packages/core/src/services/scene/ISceneService.ts +++ b/packages/core/src/services/scene/ISceneService.ts @@ -1,10 +1,13 @@ +import { IImage } from '../asset/IIconService'; import { ILayer } from '../layer/ILayerService'; import { IMapConfig } from '../map/IMapService'; import { IRenderConfig } from '../renderer/IRendererService'; + export interface ISceneService { init(config: IMapConfig & IRenderConfig): void; addLayer(layer: ILayer): void; + addImage(id: string, image: IImage): void; render(): void; destroy(): void; } diff --git a/packages/core/src/services/scene/SceneService.ts b/packages/core/src/services/scene/SceneService.ts index 4a7206f067..b41d86dfcc 100644 --- a/packages/core/src/services/scene/SceneService.ts +++ b/packages/core/src/services/scene/SceneService.ts @@ -3,6 +3,7 @@ import { inject, injectable } from 'inversify'; import { AsyncParallelHook, AsyncSeriesHook } from 'tapable'; import { TYPES } from '../../types'; import { createRendererContainer } from '../../utils/dom'; +import { IIconService, IImage } from '../asset/IIconService'; import { ICameraService, IViewport } from '../camera/ICameraService'; import { IGlobalConfig, IGlobalConfigService } from '../config/IConfigService'; import { IInteractionService } from '../interaction/IInteractionService'; @@ -12,7 +13,6 @@ import { IMapCamera, IMapService } from '../map/IMapService'; import { IRendererService } from '../renderer/IRendererService'; import { IShaderModuleService } from '../shader/IShaderModuleService'; import { ISceneService } from './ISceneService'; - /** * will emit `loaded` `resize` `destroy` event */ @@ -45,6 +45,9 @@ export default class Scene extends EventEmitter implements ISceneService { @inject(TYPES.IShaderModuleService) private readonly shaderModule: IShaderModuleService; + @inject(TYPES.IIconService) + private readonly iconService: IIconService; + /** * 是否首次渲染 */ @@ -76,6 +79,10 @@ export default class Scene extends EventEmitter implements ISceneService { public init(globalConfig: IGlobalConfig) { this.configService.setAndCheckConfig(globalConfig); + + // 初始化资源管理 字体,图片 + this.iconService.init(); + /** * 初始化底图 */ @@ -151,6 +158,9 @@ export default class Scene extends EventEmitter implements ISceneService { this.interactionService.destroy(); window.removeEventListener('resize', this.handleWindowResized, false); } + public addImage(id: string, img: IImage) { + this.iconService.addImage(id, img); + } private handleWindowResized = () => { this.emit('resize'); @@ -174,7 +184,6 @@ export default class Scene extends EventEmitter implements ISceneService { this.render(); } }; - private handleMapCameraChanged = (viewport: IViewport) => { this.cameraService.update(viewport); this.render(); diff --git a/packages/core/src/services/shader/ShaderModuleService.ts b/packages/core/src/services/shader/ShaderModuleService.ts index 843994886e..6a67709e56 100644 --- a/packages/core/src/services/shader/ShaderModuleService.ts +++ b/packages/core/src/services/shader/ShaderModuleService.ts @@ -4,6 +4,7 @@ import { extractUniforms } from '../../utils/shader-module'; import { IModuleParams, IShaderModuleService } from './IShaderModuleService'; import decode from '../../shaders/decode.glsl'; +import lighting from '../../shaders/lighting.glsl'; import projection from '../../shaders/projection.glsl'; import sdf2d from '../../shaders/sdf_2d.glsl'; @@ -21,6 +22,7 @@ export default class ShaderModuleService implements IShaderModuleService { this.registerModule('decode', { vs: decode, fs: '' }); this.registerModule('projection', { vs: projection, fs: '' }); this.registerModule('sdf_2d', { vs: '', fs: sdf2d }); + this.registerModule('lighting', { vs: lighting, fs: '' }); } public registerModule(moduleName: string, moduleParams: IModuleParams) { diff --git a/packages/core/src/services/source/ISourceService.ts b/packages/core/src/services/source/ISourceService.ts index 950fb469b6..4c86911b4f 100644 --- a/packages/core/src/services/source/ISourceService.ts +++ b/packages/core/src/services/source/ISourceService.ts @@ -12,7 +12,7 @@ type CallBack = (...args: any[]) => any; export interface ITransform { type: string; [key: string]: any; - callback: CallBack; + callback?: CallBack; } export interface ISourceCFG { diff --git a/packages/core/src/shaders/lighting.glsl b/packages/core/src/shaders/lighting.glsl new file mode 100644 index 0000000000..ced4a60329 --- /dev/null +++ b/packages/core/src/shaders/lighting.glsl @@ -0,0 +1,97 @@ +// Blinn-Phong model +// apply lighting in vertex shader instead of fragment shader +// @see https://learnopengl.com/Advanced-Lighting/Advanced-Lighting +uniform float u_ambient : 1.0; +uniform float u_diffuse : 1.0; +uniform float u_specular : 1.0; +uniform int u_num_of_directional_lights : 1; +uniform int u_num_of_spot_lights : 0; + +#define SHININESS 32.0 +#define MAX_NUM_OF_DIRECTIONAL_LIGHTS 3 +#define MAX_NUM_OF_SPOT_LIGHTS 3 + +struct DirectionalLight { + vec3 direction; + vec3 ambient; + vec3 diffuse; + vec3 specular; +}; + +struct SpotLight { + vec3 position; + vec3 direction; + vec3 ambient; + vec3 diffuse; + vec3 specular; + float constant; + float linear; + float quadratic; + float angle; + float blur; + float exponent; +}; + +uniform DirectionalLight u_directional_lights[MAX_NUM_OF_DIRECTIONAL_LIGHTS]; +uniform SpotLight u_spot_lights[MAX_NUM_OF_SPOT_LIGHTS]; + +vec3 calc_directional_light(DirectionalLight light, vec3 normal, vec3 viewDir) { + vec3 lightDir = normalize(light.direction); + // diffuse shading + float diff = max(dot(normal, lightDir), 0.0); + // Blinn-Phong specular shading + vec3 halfwayDir = normalize(lightDir + viewDir); + float spec = pow(max(dot(normal, halfwayDir), 0.0), SHININESS); + + vec3 ambient = light.ambient * u_ambient; + vec3 diffuse = light.diffuse * diff * u_diffuse; + vec3 specular = light.specular * spec * u_specular; + + return ambient + diffuse + specular; +} + +// vec3 calc_spot_light(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir) { +// vec3 lightDir = normalize(light.position - fragPos); +// // diffuse shading +// float diff = max(dot(normal, lightDir), 0.0); +// // specular shading +// vec3 reflectDir = reflect(-lightDir, normal); +// float spec = pow(max(dot(viewDir, reflectDir), 0.0), SHININESS); +// // attenuation +// float distance = length(light.position - fragPos); +// float attenuation = 1.0 / (light.constant + light.linear * distance + +// light.quadratic * (distance * distance)); + +// vec3 ambient = light.ambient * u_ambient; +// vec3 diffuse = light.diffuse * diff * u_diffuse; +// vec3 specular = light.specular * spec * u_specular; + +// float spotEffect = dot(normalize(light.direction), -lightDir); +// float spotCosCutoff = cos(light.angle / 180.0 * PI); +// float spotCosOuterCutoff = cos((light.angle + light.blur) / 180.0 * PI); +// float spotCosInnerCutoff = cos((light.angle - light.blur) / 180.0 * PI); +// if (spotEffect > spotCosCutoff) { +// spotEffect = pow(smoothstep(spotCosOuterCutoff, spotCosInnerCutoff, spotEffect), light.exponent); +// } else { +// spotEffect = 0.0; +// } + +// return ambient + attenuation * (spotEffect * diffuse + specular); +// } + +vec3 calc_lighting(vec3 position, vec3 normal, vec3 viewDir) { + vec3 weight = vec3(0.0); + for (int i = 0; i < MAX_NUM_OF_DIRECTIONAL_LIGHTS; i++) { + if (i >= u_num_of_directional_lights) { + break; + } + weight += calc_directional_light(u_directional_lights[i], normal, viewDir); + } + // for (int i = 0; i < MAX_NUM_OF_SPOT_LIGHTS; i++) { + // if (i >= u_num_of_spot_lights) { + // break; + // } + // weight += calc_spot_light(u_spot_lights[i], normal, position, viewDir); + // } + return weight; +} \ No newline at end of file diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index efeaa99a5a..b6af81e25e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -9,6 +9,7 @@ const TYPES = { IMapService: Symbol.for('IMapService'), IRendererService: Symbol.for('IRendererService'), IShaderModuleService: Symbol.for('IShaderModuleService'), + IIconService: Symbol.for('IIconService'), IInteractionService: Symbol.for('IInteractionService'), /** multi-pass */ diff --git a/packages/core/src/utils/font_util.ts b/packages/core/src/utils/font_util.ts new file mode 100644 index 0000000000..68b9260645 --- /dev/null +++ b/packages/core/src/utils/font_util.ts @@ -0,0 +1,68 @@ +import { + IIcon, + IICONMap, + IIconService, + IIconValue, + IImage, +} from '../services/asset/IIconService'; +export function buildIconMaping( + icons: IIcon[], + buffer: number, + maxCanvasWidth: number, +) { + let xOffset = 0; + let yOffset = 0; + let rowHeight = 0; + let columns = []; + const mapping: IICONMap = {}; + for (const icon of icons) { + if (!mapping[icon.id]) { + const { height, width } = icon; + + // fill one row + if (xOffset + width + buffer > maxCanvasWidth) { + buildRowMapping(mapping, columns, yOffset); + + xOffset = 0; + yOffset = rowHeight + yOffset + buffer; + rowHeight = 0; + columns = []; + } + + columns.push({ + icon, + xOffset, + }); + + xOffset = xOffset + width + buffer; + rowHeight = Math.max(rowHeight, height); + } + } + + if (columns.length > 0) { + buildRowMapping(mapping, columns, yOffset); + } + + const canvasHeight = nextPowOfTwo(rowHeight + yOffset + buffer); + + return { + mapping, + canvasHeight, + }; +} +function buildRowMapping( + mapping: IICONMap, + columns: Array<{ + icon: IIcon; + xOffset: number; + }>, + yOffset: number, +) { + for (const column of columns) { + const { icon, xOffset } = column; + mapping[icon.id] = { ...icon, x: xOffset, y: yOffset, image: icon.image }; + } +} +export function nextPowOfTwo(num: number) { + return Math.pow(2, Math.ceil(Math.log2(num))); +} diff --git a/packages/layers/package.json b/packages/layers/package.json index f1390a2970..431b0345cc 100644 --- a/packages/layers/package.json +++ b/packages/layers/package.json @@ -22,11 +22,16 @@ "@l7/core": "^0.0.1", "@l7/source": "^0.0.1", "@turf/meta": "^6.0.2", + "@types/d3-color": "^1.2.2", "d3-array": "^2.3.1", + "d3-color": "^1.4.0", "d3-scale": "^3.1.0", "earcut": "^2.2.1", "eventemitter3": "^3.1.0", + "gl-matrix": "^3.1.0", + "gl-vec2": "^1.3.0", "lodash": "^4.17.15", + "polyline-miter-util": "^1.0.1", "tapable": "^2.0.0-beta.8" }, "devDependencies": { diff --git a/packages/layers/src/core/BaseBuffer.ts b/packages/layers/src/core/BaseBuffer.ts index 5fd12f9d6a..c4a977af49 100644 --- a/packages/layers/src/core/BaseBuffer.ts +++ b/packages/layers/src/core/BaseBuffer.ts @@ -1,17 +1,22 @@ +import { IICONMap, ILayerStyleOptions } from '@l7/core'; +import { lngLatToMeters } from '@l7/utils'; +import { vec3 } from 'gl-matrix'; +import { IExtrudeGeomety } from '../point/shape/extrude'; interface IBufferCfg { data: unknown[]; - imagePos?: unknown; - uv?: boolean; + iconMap?: IICONMap; + style?: ILayerStyleOptions; } -type Position = number[]; +export type Position = number[]; type Color = [number, number, number, number]; export interface IBufferInfo { vertices?: any; indexArray?: any; indexOffset: any; - verticesOffset: any; + verticesOffset: number; faceNum?: any; dimensions: number; + [key: string]: any; } export interface IEncodeFeature { color?: Color; @@ -19,9 +24,10 @@ export interface IEncodeFeature { shape?: string | number; pattern?: string; id?: number; - coordinates: Position[][]; - bufferInfo: IBufferInfo; + coordinates: unknown; + bufferInfo: unknown; } + export default class Buffer { public attributes: { [key: string]: Float32Array; @@ -29,17 +35,64 @@ export default class Buffer { public verticesCount: number = 0; public indexArray: Uint32Array = new Uint32Array(0); public indexCount: number = 0; - + public instanceGeometry: IExtrudeGeomety; protected data: unknown[]; - protected imagePos: unknown; - protected uv: boolean; + protected iconMap: IICONMap; + protected style: any; - constructor({ data, imagePos, uv }: IBufferCfg) { + constructor({ data, iconMap, style }: IBufferCfg) { this.data = data; - this.imagePos = imagePos; - this.uv = !!uv; + this.iconMap = iconMap as IICONMap; + this.style = style; this.init(); } + public computeVertexNormals( + field: string = 'positions', + flag: boolean = true, + ) { + const normals = (this.attributes.normals = new Float32Array( + this.verticesCount * 3, + )); + const indexArray = this.indexArray; + const positions = this.attributes[field]; + let vA; + let vB; + let vC; + const cb = vec3.create(); + const ab = vec3.create(); + const normal = vec3.create(); + for (let i = 0, li = indexArray.length; i < li; i += 3) { + vA = indexArray[i + 0] * 3; + vB = indexArray[i + 1] * 3; + vC = indexArray[i + 2] * 3; + const [ax, ay] = flag + ? lngLatToMeters([positions[vA], positions[vA + 1]]) + : [positions[vA], positions[vA + 1]]; + const pA = vec3.fromValues(ax, ay, positions[vA + 2]); + const [bx, by] = flag + ? lngLatToMeters([positions[vB], positions[vB + 1]]) + : [positions[vB], positions[vB + 1]]; + const pB = vec3.fromValues(bx, by, positions[vB + 2]); + const [cx, cy] = flag + ? lngLatToMeters([positions[vC], positions[vC + 1]]) + : [positions[vC], positions[vC + 1]]; + const pC = vec3.fromValues(cx, cy, positions[vC + 2]); + vec3.sub(cb, pC, pB); + vec3.sub(ab, pA, pB); + vec3.cross(normal, cb, ab); + normals[vA] += cb[0]; + normals[vA + 1] += cb[1]; + normals[vA + 2] += cb[2]; + normals[vB] += cb[0]; + normals[vB + 1] += cb[1]; + normals[vB + 2] += cb[2]; + normals[vC] += cb[0]; + normals[vC + 1] += cb[1]; + normals[vC + 2] += cb[2]; + } + this.normalizeNormals(); + } + // 计算每个要素顶点个数,记录索引位置 protected calculateFeatures() { throw new Error('Method not implemented.'); @@ -47,11 +100,13 @@ export default class Buffer { protected buildFeatures() { throw new Error('Method not implemented.'); } + protected checkIsClosed(points: Position[][]) { const p1 = points[0][0]; const p2 = points[0][points[0].length - 1]; return p1[0] === p2[0] && p1[1] === p2[1]; } + protected concat(arrayType: Float32Array, arrays: any) { let totalLength = 0; for (const arr of arrays) { @@ -71,8 +126,9 @@ export default class Buffer { } protected encodeArray(feature: IEncodeFeature, num: number) { const { color, id, pattern, size } = feature; - const { verticesOffset } = feature.bufferInfo; - const imagePos = this.imagePos; + const bufferInfo = feature.bufferInfo as IBufferInfo; + const { verticesOffset } = bufferInfo; + const imagePos = this.iconMap; const start1 = verticesOffset; for (let i = 0; i < num; i++) { if (color) { @@ -88,7 +144,7 @@ export default class Buffer { let size2: number[] = []; if (Array.isArray(size) && size.length === 2) { // TODO 多维size支持 - size2 = [size[0]]; + size2 = [size[0], size[0], size[1]]; } if (!Array.isArray(size)) { size2 = [size]; @@ -103,82 +159,35 @@ export default class Buffer { } } } - protected calculateWall(feature: IEncodeFeature) { - const size = feature.size; - const { - vertices, - indexOffset, - verticesOffset, - faceNum, - dimensions, - } = feature.bufferInfo; - this.encodeArray(feature, faceNum * 4); - for (let i = 0; i < faceNum; i++) { - const prePoint = vertices.slice(i * 3, i * 3 + 3); - const nextPoint = vertices.slice(i * 3 + 3, i * 3 + 6); - this.calculateExtrudeFace( - prePoint, - nextPoint, - verticesOffset + i * 4, - indexOffset + i * 6, - size as number, - ); - feature.bufferInfo.verticesOffset += 4; - feature.bufferInfo.indexOffset += 6; - } - } - - protected calculateExtrudeFace( - prePoint: number[], - nextPoint: number[], - positionOffset: number, - indexOffset: number | undefined, - size: number, - ) { - this.attributes.positions.set( - [ - prePoint[0], - prePoint[1], - size, - nextPoint[0], - nextPoint[1], - size, - prePoint[0], - prePoint[1], - 0, - nextPoint[0], - nextPoint[1], - 0, - ], - positionOffset * 3, - ); - const indexArray = [1, 2, 0, 3, 2, 1].map((v) => { - return v + positionOffset; - }); - if (this.uv) { - this.attributes.uv.set( - [0.1, 0, 0, 0, 0.1, size / 2000, 0, size / 2000], - positionOffset * 2, - ); - } - this.indexArray.set(indexArray, indexOffset); - } - - private init() { - this.calculateFeatures(); - this.initAttributes(); - this.buildFeatures(); - } - - private initAttributes() { + protected initAttributes() { this.attributes.positions = new Float32Array(this.verticesCount * 3); this.attributes.colors = new Float32Array(this.verticesCount * 4); this.attributes.pickingIds = new Float32Array(this.verticesCount); this.attributes.sizes = new Float32Array(this.verticesCount); this.attributes.pickingIds = new Float32Array(this.verticesCount); - if (this.uv) { - this.attributes.uv = new Float32Array(this.verticesCount * 2); - } this.indexArray = new Uint32Array(this.indexCount); } + + private init() { + // 1. 计算 attribute 长度 + this.calculateFeatures(); + // 2. 初始化 attribute + this.initAttributes(); + // 3. 拼接attribute + this.buildFeatures(); + } + + private normalizeNormals() { + const { normals } = this.attributes; + for (let i = 0, li = normals.length; i < li; i += 3) { + const normal = vec3.fromValues( + normals[i], + normals[i + 1], + normals[i + 2], + ); + const newNormal = vec3.create(); + vec3.normalize(newNormal, normal); + normals.set(newNormal, i); + } + } } diff --git a/packages/layers/src/core/BaseLayer.ts b/packages/layers/src/core/BaseLayer.ts index 0b429cd837..a757000890 100644 --- a/packages/layers/src/core/BaseLayer.ts +++ b/packages/layers/src/core/BaseLayer.ts @@ -1,20 +1,26 @@ import { IGlobalConfigService, + IIconService, ILayer, ILayerInitializationOptions, ILayerPlugin, ILayerStyleAttribute, + ILayerStyleOptions, IModel, IMultiPassRenderer, IRendererService, + ISource, + ISourceCFG, lazyInject, StyleAttributeField, + StyleAttributeOption, TYPES, } from '@l7/core'; -import Source, { ISourceCFG } from '@l7/source'; +import Source from '@l7/source'; import { isFunction } from 'lodash'; import { SyncHook } from 'tapable'; import DataEncodePlugin from '../plugins/DataEncodePlugin'; +import DataSourcePlugin from '../plugins/DataSourcePlugin'; import MultiPassRendererPlugin from '../plugins/MultiPassRendererPlugin'; import ShaderUniformPlugin from '../plugins/ShaderUniformPlugin'; import StyleAttribute from './StyleAttribute'; @@ -37,15 +43,24 @@ export default class BaseLayer implements ILayer { // 插件集 public plugins: ILayerPlugin[] = [ + new DataSourcePlugin(), new DataEncodePlugin(), new MultiPassRendererPlugin(), new ShaderUniformPlugin(), ]; - + public sourceOption: { + data: any; + options?: ISourceCFG; + }; + public styleOption: ILayerStyleOptions = { + opacity: 1.0, + }; // 样式属性 public styleAttributes: { - [key: string]: Partial; + [key: string]: Required; } = {}; + @lazyInject(TYPES.IIconService) + protected readonly iconService: IIconService; protected layerSource: Source; @@ -77,8 +92,10 @@ export default class BaseLayer implements ILayer { this.buildModels(); return this; } - - public color(field: StyleAttributeField, values?: any) { + public color( + field: StyleAttributeField, + values?: StyleAttributeOption, + ): ILayer { this.createStyleAttribute( 'color', field, @@ -88,7 +105,10 @@ export default class BaseLayer implements ILayer { return this; } - public size(field: StyleAttributeField, values?: any) { + public size( + field: StyleAttributeField, + values?: StyleAttributeOption, + ): ILayer { this.createStyleAttribute( 'size', field, @@ -98,7 +118,10 @@ export default class BaseLayer implements ILayer { return this; } - public shape(field: StyleAttributeField, values?: any) { + public shape( + field: StyleAttributeField, + values?: StyleAttributeOption, + ): ILayer { this.createStyleAttribute( 'shape', field, @@ -108,15 +131,17 @@ export default class BaseLayer implements ILayer { return this; } - public source(data: any, options?: ISourceCFG) { - this.layerSource = new Source(data, options); + public source(data: any, options?: ISourceCFG): ILayer { + this.sourceOption = { + data, + options, + }; return this; } - - public style(styleAttributes: any) { - // + public style(options: ILayerStyleOptions): ILayer { + this.styleOption = options; + return this; } - public render(): ILayer { if (this.multiPassRenderer && this.multiPassRenderer.getRenderFlag()) { this.multiPassRenderer.render(); @@ -131,11 +156,14 @@ export default class BaseLayer implements ILayer { } public getStyleAttributes(): { - [key: string]: Partial; + [key: string]: Required; } { return this.styleAttributes; } + public setSource(source: Source) { + this.layerSource = source; + } public getSource() { return this.layerSource; } diff --git a/packages/layers/src/core/ScaleController.ts b/packages/layers/src/core/ScaleController.ts index ae1e99b404..849e10ac01 100644 --- a/packages/layers/src/core/ScaleController.ts +++ b/packages/layers/src/core/ScaleController.ts @@ -1,4 +1,9 @@ -import { IScale, ScaleTypes } from '@l7/core'; +import { + IScaleOption, + IStyleScale, + ScaleTypes, + StyleScaleType, +} from '@l7/core'; import { extent } from 'd3-array'; import * as d3 from 'd3-scale'; import { isNil, isNumber, isString, uniq } from 'lodash'; @@ -19,27 +24,28 @@ const scaleMap = { export default class ScaleController { private scaleOptions: { - [field: string]: IScale; + [field: string]: IScaleOption; }; - constructor(cfg: { [field: string]: IScale }) { + constructor(cfg: { [field: string]: IScaleOption }) { this.scaleOptions = cfg; } - public createScale( - field: string, - data?: any[], - ): { field: string; scale: any } { - let scaleOption: IScale = this.scaleOptions[field]; - const scale: { field: string; scale: any } = { + public createScale(field: string, data?: any[]): IStyleScale { + let scaleOption: IScaleOption = this.scaleOptions[field]; + const scale: IStyleScale = { field, scale: undefined, + type: StyleScaleType.VARIABLE, + option: scaleOption, }; if (!data || !data.length) { // 数据为空 - scale.scale = - scaleOption && scaleOption.type - ? this.generateScale(scaleOption.type, scaleOption) - : d3.scaleOrdinal([field]); + if (scaleOption && scaleOption.type) { + scale.scale = this.generateScale(scaleOption.type, scaleOption); + } else { + scale.scale = d3.scaleOrdinal([field]); + scale.type = StyleScaleType.CONSTANT; + } return scale; } let firstValue = null; @@ -53,6 +59,7 @@ export default class ScaleController { // 常量 Scale if (isNumber(field) || (isNil(firstValue) && !scaleOption)) { scale.scale = d3.scaleOrdinal([field]); + scale.type = StyleScaleType.CONSTANT; } else { // 根据数据类型判断 默认等分位,时间,和枚举类型 const type = @@ -61,8 +68,9 @@ export default class ScaleController { : this.getDefaultType(field, firstValue); const cfg = this.getScaleCfg(type, field, data); Object.assign(cfg, scaleOption); - scaleOption = cfg; + scaleOption = cfg; // 更新scale配置 scale.scale = this.generateScale(type, cfg); + scale.option = scaleOption; } return scale; } @@ -78,7 +86,7 @@ export default class ScaleController { } private getScaleCfg(type: ScaleTypes, field: string, data: any[]) { - const cfg: IScale = { + const cfg: IScaleOption = { field, type, }; @@ -92,11 +100,12 @@ export default class ScaleController { return cfg; } - private generateScale(type: ScaleTypes, scaleOption: IScale) { + private generateScale(type: ScaleTypes, scaleOption: IScaleOption) { // @ts-ignore - const scale = scaleMap[type](); + let scale = scaleMap[type](); if (scaleOption.hasOwnProperty('domain')) { - scale.domain(scaleOption.domain); + // 处理同一字段映射不同视觉通道的问题 + scale = scale.copy().domain(scaleOption.domain); } // TODO 其他属性支持 return scale; diff --git a/packages/layers/src/core/StyleAttribute.ts b/packages/layers/src/core/StyleAttribute.ts index f8353b18c3..459e0bc358 100644 --- a/packages/layers/src/core/StyleAttribute.ts +++ b/packages/layers/src/core/StyleAttribute.ts @@ -1,18 +1,17 @@ -import { ILayerStyleAttribute } from '@l7/core'; -import { isNil } from 'lodash'; +import { ILayerStyleAttribute, IStyleScale, StyleScaleType } from '@l7/core'; +import { isNil, isString } from 'lodash'; type CallBack = (...args: any[]) => any; export default class StyleAttribute implements ILayerStyleAttribute { - public type: string; + public type: StyleScaleType; public names: string[]; - public scales: any[] = []; + public scales: IStyleScale[] = []; public values: any[] = []; public field: string; constructor(cfg: any) { const { - type = 'base', - names = [], + type = StyleScaleType.CONSTANT, scales = [], values = [], callback, @@ -22,11 +21,10 @@ export default class StyleAttribute implements ILayerStyleAttribute { this.type = type; this.scales = scales; this.values = values; - this.names = names; - // 设置 range TODO 2维映射 - this.scales.forEach((scale) => { - scale.scale.range(values); - }); + this.names = this.parseFields(field) || []; + if (callback) { + this.type = StyleScaleType.VARIABLE; + } this.callback = (...params: any[]): any[] => { /** * 当用户设置的 callback 返回 null 时, 应该返回默认 callback 中的值 @@ -44,7 +42,22 @@ export default class StyleAttribute implements ILayerStyleAttribute { }; } public callback: CallBack = () => []; - + public setScales(scales: IStyleScale[]): void { + if (scales.some((scale) => scale.type === StyleScaleType.VARIABLE)) { + this.type = StyleScaleType.VARIABLE; + scales.forEach((scale) => { + if (this.values.length > 0) { + scale.scale.range(this.values); + } + }); + } else { + // 设置attribute 常量值 + this.values = scales.map((scale, index) => { + return scale.scale(this.names[index]); + }); + } + this.scales = scales; + } public mapping(...params: unknown[]): unknown[] { return this.callback.apply(this, params); } @@ -60,4 +73,18 @@ export default class StyleAttribute implements ILayerStyleAttribute { return value; }); } + /** + * @example + * 'w*h' => ['w', 'h'] + * 'w' => ['w'] + */ + private parseFields(field: string[] | string): string[] { + if (Array.isArray(field)) { + return field; + } + if (isString(field)) { + return field.split('*'); + } + return [field]; + } } diff --git a/packages/layers/src/heatmap/buffers/GridBuffer.ts b/packages/layers/src/heatmap/buffers/GridBuffer.ts new file mode 100644 index 0000000000..b75f64fc16 --- /dev/null +++ b/packages/layers/src/heatmap/buffers/GridBuffer.ts @@ -0,0 +1,46 @@ +import BufferBase, { IEncodeFeature, Position } from '../../core/BaseBuffer'; +import extrudePolygon, { + fillPolygon, + IExtrudeGeomety, +} from '../../point/shape/extrude'; +import { + geometryShape, + ShapeType2D, + ShapeType3D, +} from '../../point/shape/Path'; +export default class GridHeatMapBuffer extends BufferBase { + private verticesOffset: number = 0; + protected buildFeatures() { + this.verticesOffset = 0; + const layerData = this.data as IEncodeFeature[]; + layerData.forEach((feature: IEncodeFeature) => { + this.calculateFill(feature); + }); + } + protected calculateFeatures() { + const layerData = this.data as IEncodeFeature[]; + const shape = layerData[0].shape as ShapeType3D | ShapeType2D; + this.verticesCount = layerData.length; + this.indexCount = 0; + this.instanceGeometry = this.getGeometry(shape as + | ShapeType2D + | ShapeType3D); + } + protected calculateFill(feature: IEncodeFeature) { + feature.bufferInfo = { verticesOffset: this.verticesOffset }; + const coordinates = feature.coordinates as Position; + this.encodeArray(feature, 1); + this.attributes.positions.set([...coordinates, 1], this.verticesOffset * 3); + this.verticesOffset++; + } + private getGeometry(shape: ShapeType2D | ShapeType3D): IExtrudeGeomety { + const path = geometryShape[shape] + ? geometryShape[shape]() + : geometryShape.circle(); + // const geometry = ShapeType2D[str as ShapeType2D] + // ? fillPolygon([path]) + // : extrudePolygon([path]); + const geometry = fillPolygon([path]); + return geometry; + } +} diff --git a/packages/layers/src/heatmap/index.ts b/packages/layers/src/heatmap/index.ts new file mode 100644 index 0000000000..aa30d0e77c --- /dev/null +++ b/packages/layers/src/heatmap/index.ts @@ -0,0 +1,114 @@ +import { + gl, + IRendererService, + IShaderModuleService, + lazyInject, + TYPES, +} from '@l7/core'; +import BaseLayer from '../core/BaseLayer'; +import GridHeatMapBuffer from './buffers/GridBuffer'; +import hexagon_frag from './shaders/hexagon_frag.glsl'; +import hexagon_vert from './shaders/hexagon_vert.glsl'; + +export default class HeatMapLayer extends BaseLayer { + public name: string = 'HeatMapLayer'; + + @lazyInject(TYPES.IShaderModuleService) + private readonly shaderModule: IShaderModuleService; + + @lazyInject(TYPES.IRendererService) + private readonly renderer: IRendererService; + + protected renderModels() { + this.models.forEach((model) => + model.draw({ + uniforms: { + u_ModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + }, + }), + ); + return this; + } + + protected buildModels(): void { + this.shaderModule.registerModule('grid', { + vs: hexagon_vert, + fs: hexagon_frag, + }); + this.models = []; + const { vs, fs, uniforms } = this.shaderModule.getModule('grid'); + const buffer = new GridHeatMapBuffer({ + data: this.getEncodedData(), + }); + console.log(this.getSource()); + console.log(buffer); + const { + createAttribute, + createBuffer, + createElements, + createModel, + } = this.renderer; + + this.models.push( + createModel({ + attributes: { + a_miter: createAttribute({ + buffer: createBuffer({ + data: buffer.instanceGeometry.positions, + type: gl.FLOAT, + }), + size: 3, + divisor: 0, + }), + // a_normal: createAttribute({ + // buffer: createBuffer({ + // data: buffer.attributes.normals, + // type: gl.FLOAT, + // }), + // size: 3, + // }), + a_color: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.colors, + type: gl.FLOAT, + }), + size: 4, + divisor: 1, + }), + // a_size: createAttribute({ + // buffer: createBuffer({ + // data: buffer.attributes.sizes, + // type: gl.FLOAT, + // }), + // size: 1, + // divisor: 1, + // }), + a_Position: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.positions, + type: gl.FLOAT, + }), + size: 3, + divisor: 1, + }), + }, + uniforms: { + ...uniforms, + u_opacity: (this.styleOption.opacity as number) || 1.0, + u_radius: [ + this.getSource().data.xOffset, + this.getSource().data.yOffset, + ], + }, + fs, + vs, + count: buffer.instanceGeometry.index.length, + instances: buffer.verticesCount, + elements: createElements({ + data: buffer.instanceGeometry.index, + type: gl.UNSIGNED_INT, + }), + }), + ); + } +} diff --git a/packages/layers/src/heatmap/shaders/hexagon_frag.glsl b/packages/layers/src/heatmap/shaders/hexagon_frag.glsl new file mode 100644 index 0000000000..c9944e0a01 --- /dev/null +++ b/packages/layers/src/heatmap/shaders/hexagon_frag.glsl @@ -0,0 +1,7 @@ +precision highp float; +varying vec4 v_color; +uniform float u_opacity: 0.1; +void main() { + gl_FragColor = v_color; + gl_FragColor.a *= u_opacity; +} \ No newline at end of file diff --git a/packages/layers/src/heatmap/shaders/hexagon_vert.glsl b/packages/layers/src/heatmap/shaders/hexagon_vert.glsl new file mode 100644 index 0000000000..36bd09e2c0 --- /dev/null +++ b/packages/layers/src/heatmap/shaders/hexagon_vert.glsl @@ -0,0 +1,18 @@ +precision highp float; +attribute vec3 a_Position; +attribute vec3 a_miter; +attribute float a_size; +attribute vec4 a_color; +uniform vec2 u_radius; +uniform float u_coverage: 1.; +uniform float u_angle: 0; +uniform mat4 u_ModelMatrix; +varying vec4 v_color; +#pragma include "projection" +void main() { + v_color = a_color; + mat2 rotationMatrix = mat2(cos(u_angle), sin(u_angle), -sin(u_angle), cos(u_angle)); + vec2 offset =(vec2(a_miter.xy * u_radius * u_coverage * rotationMatrix)); + vec4 project_pos = project_position(vec4(a_Position.xy + offset, 0, 1.0)); + gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy, 0., 1.0)); +} \ No newline at end of file diff --git a/packages/layers/src/index.ts b/packages/layers/src/index.ts index 13d132e024..91a9c56138 100644 --- a/packages/layers/src/index.ts +++ b/packages/layers/src/index.ts @@ -1,5 +1,16 @@ import BaseLayer from './core/BaseLayer'; +import HeatMapLayer from './heatmap'; +import Line from './line'; import PointLayer from './point'; +import Point from './point/point'; import PolygonLayer from './polygon'; - -export { BaseLayer, PointLayer, PolygonLayer }; +import ImageLayer from './raster'; +export { + BaseLayer, + PointLayer, + PolygonLayer, + Point, + Line, + ImageLayer, + HeatMapLayer, +}; diff --git a/packages/layers/src/line/buffers/line.ts b/packages/layers/src/line/buffers/line.ts new file mode 100644 index 0000000000..522b124220 --- /dev/null +++ b/packages/layers/src/line/buffers/line.ts @@ -0,0 +1,95 @@ +import { lngLatToMeters, Point } from '@l7/utils'; +import BufferBase, { IEncodeFeature, Position } from '../../core/BaseBuffer'; +import getNormals from '../../utils/polylineNormal'; +interface IBufferInfo { + normals: number[]; + arrayIndex: number[]; + positions: number[]; + attrDistance: number[]; + miters: number[]; + verticesOffset: number; + indexOffset: number; +} +export default class LineBuffer extends BufferBase { + private hasPattern: boolean; + protected buildFeatures() { + const layerData = this.data as IEncodeFeature[]; + layerData.forEach((feature: IEncodeFeature) => { + this.calculateLine(feature); + delete feature.bufferInfo; + }); + this.hasPattern = layerData.some((feature: IEncodeFeature) => { + return feature.pattern; + }); + } + protected initAttributes() { + super.initAttributes(); + this.attributes.dashArray = new Float32Array(this.verticesCount); + this.attributes.attrDistance = new Float32Array(this.verticesCount); + this.attributes.totalDistances = new Float32Array(this.verticesCount); + this.attributes.patterns = new Float32Array(this.verticesCount * 2); + this.attributes.miters = new Float32Array(this.verticesCount); + this.attributes.normals = new Float32Array(this.verticesCount * 3); + } + protected calculateFeatures() { + const layerData = this.data as IEncodeFeature[]; + // 计算长 + layerData.forEach((feature: IEncodeFeature, index: number) => { + let coordinates = feature.coordinates as Position[] | Position[][]; + if (Array.isArray(coordinates[0][0])) { + coordinates = coordinates[0] as Position[]; + } + // @ts-ignore + const projectCoord: number[][] = coordinates.map((item: Position[]) => { + // @ts-ignore + const p: Point = [...item]; + return lngLatToMeters(p); + }); + const { normals, attrIndex, attrPos, attrDistance, miters } = getNormals( + coordinates as number[][], + false, + this.verticesCount, + ); + const bufferInfo: IBufferInfo = { + normals, + arrayIndex: attrIndex, + positions: attrPos, + attrDistance, + miters, + verticesOffset: this.verticesCount, + indexOffset: this.indexCount, + }; + this.verticesCount += attrPos.length / 3; + this.indexCount += attrIndex.length; + feature.bufferInfo = bufferInfo; + }); + } + private calculateLine(feature: IEncodeFeature) { + const bufferInfo = feature.bufferInfo as IBufferInfo; + const { + normals, + arrayIndex, + positions, + attrDistance, + miters, + verticesOffset, + indexOffset, + } = bufferInfo; + const { dashArray = 200 } = this.style; + + this.encodeArray(feature, positions.length / 3); + const totalLength = attrDistance[attrDistance.length - 1]; + // 增加长度 + const totalDistances = Array(positions.length / 3).fill(totalLength); + // 虚线比例 + const ratio = dashArray / totalLength; + const dashArrays = Array(positions.length / 3).fill(ratio); + this.attributes.positions.set(positions, verticesOffset * 3); + this.indexArray.set(arrayIndex, indexOffset); + this.attributes.miters.set(miters, verticesOffset); + this.attributes.normals.set(normals, verticesOffset * 3); + this.attributes.attrDistance.set(attrDistance, verticesOffset); + this.attributes.totalDistances.set(totalDistances, verticesOffset); + this.attributes.dashArray.set(dashArrays, verticesOffset); + } +} diff --git a/packages/layers/src/line/index.ts b/packages/layers/src/line/index.ts new file mode 100644 index 0000000000..817200e646 --- /dev/null +++ b/packages/layers/src/line/index.ts @@ -0,0 +1,103 @@ +import { + gl, + IRendererService, + IShaderModuleService, + lazyInject, + TYPES, +} from '@l7/core'; +import BaseLayer from '../core/BaseLayer'; +import LineBuffer from './buffers/line'; +import line_frag from './shaders/line_frag.glsl'; +import line_vert from './shaders/line_vert.glsl'; +export default class LineLayer extends BaseLayer { + public name: string = 'LineLayer'; + @lazyInject(TYPES.IShaderModuleService) + private readonly shaderModule: IShaderModuleService; + + @lazyInject(TYPES.IRendererService) + private readonly renderer: IRendererService; + + protected renderModels() { + this.models.forEach((model) => + model.draw({ + uniforms: { + u_ModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + }, + }), + ); + return this; + } + protected buildModels(): void { + this.shaderModule.registerModule('line', { + vs: line_vert, + fs: line_frag, + }); + + this.models = []; + const { vs, fs, uniforms } = this.shaderModule.getModule('line'); + const buffer = new LineBuffer({ + data: this.getEncodedData(), + style: this.styleOption, + }); + console.log(buffer); + const { + createAttribute, + createBuffer, + createElements, + createModel, + } = this.renderer; + + this.models.push( + createModel({ + attributes: { + a_Position: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.positions, + type: gl.FLOAT, + }), + size: 3, + }), + a_normal: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.normals, + type: gl.FLOAT, + }), + size: 3, + }), + a_color: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.colors, + type: gl.FLOAT, + }), + size: 4, + }), + a_size: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.sizes, + type: gl.FLOAT, + }), + size: 1, + }), + a_miter: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.miters, + type: gl.FLOAT, + }), + size: 1, + }), + }, + uniforms: { + ...uniforms, + u_opacity: this.styleOption.opacity as number, + }, + fs, + vs, + count: buffer.indexArray.length, + elements: createElements({ + data: buffer.indexArray, + type: gl.UNSIGNED_INT, + }), + }), + ); + } +} diff --git a/packages/layers/src/line/shaders/line_frag.glsl b/packages/layers/src/line/shaders/line_frag.glsl new file mode 100644 index 0000000000..8379af0d5e --- /dev/null +++ b/packages/layers/src/line/shaders/line_frag.glsl @@ -0,0 +1,9 @@ +uniform float u_blur : 0.99; +varying vec4 v_color; +varying vec3 v_normal; +void main() { + gl_FragColor = v_color; + // anti-alias + float blur = smoothstep(u_blur, 1., length(v_normal.xy)); + gl_FragColor.a *= blur; +} \ No newline at end of file diff --git a/packages/layers/src/line/shaders/line_vert.glsl b/packages/layers/src/line/shaders/line_vert.glsl new file mode 100644 index 0000000000..fd3bc6d43e --- /dev/null +++ b/packages/layers/src/line/shaders/line_vert.glsl @@ -0,0 +1,23 @@ + +attribute float a_miter; +attribute vec4 a_color; +attribute float a_size; +attribute float a_distance; +attribute float a_dash_array; +attribute float a_total_distance; +attribute vec3 a_normal; +attribute vec3 a_Position; +uniform mat4 u_ModelMatrix; + +varying vec4 v_color; +varying float v_dash_array; +varying vec3 v_normal; +#pragma include "projection" +void main() { + v_normal = a_normal; + v_color = a_color; + vec3 size = a_miter * a_size * v_normal; + vec2 offset = project_pixel(size.xy); + vec4 project_pos = project_position(vec4(a_Position.xy, 0, 1.0)); + gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy + offset, 0, 1.0)); +} diff --git a/packages/layers/src/plugins/DataEncodePlugin.ts b/packages/layers/src/plugins/DataEncodePlugin.ts index 32f59bcbcc..39255bb131 100644 --- a/packages/layers/src/plugins/DataEncodePlugin.ts +++ b/packages/layers/src/plugins/DataEncodePlugin.ts @@ -4,7 +4,9 @@ import { ILayerPlugin, ILayerStyleAttribute, IParseDataItem, + IStyleScale, lazyInject, + StyleScaleType, TYPES, } from '@l7/core'; import { isString } from 'lodash'; @@ -18,10 +20,7 @@ export default class DataEncodePlugin implements ILayerPlugin { private scaleController: ScaleController; private scaleCache: { - [fieldName: string]: { - field: string; - scale: any; - }; + [fieldName: string]: IStyleScale; } = {}; public apply(layer: ILayer) { @@ -39,14 +38,12 @@ export default class DataEncodePlugin implements ILayerPlugin { // create scales by source data & config Object.keys(layer.styleAttributes).forEach((attributeName) => { const attribute = layer.styleAttributes[attributeName]; - const fields = this.parseFields(attribute.field || ''); const scales: any[] = []; - fields.forEach((field: string) => { - scales.push(this.getOrCreateScale(attribute, dataArray)); + attribute.names.forEach((field: string) => { + scales.push(this.getOrCreateScale(attribute, field, dataArray)); }); - attribute.scales = scales; + attribute.setScales(scales); }); - // mapping with source data layer.setEncodedData(this.mapping(layer.styleAttributes, dataArray)); }); @@ -55,33 +52,28 @@ export default class DataEncodePlugin implements ILayerPlugin { // layer.hooks.beforeRender.tap() } - private getOrCreateScale(attribute: ILayerStyleAttribute, data: any[]) { - const { field } = attribute; + private getOrCreateScale( + attribute: ILayerStyleAttribute, + field: string, + data: any[], + ): IStyleScale { let scale = this.scaleCache[field as string]; if (!scale) { scale = this.scaleController.createScale(field as string, data); - scale.scale.range(attribute.values); + if ( + scale.type === StyleScaleType.VARIABLE && + attribute.values && + attribute.values.length > 0 + ) { + scale.scale.range(attribute.values); + } this.scaleCache[field as string] = scale; } - // scale: scale.scale.copy(), - return this.scaleCache[field as string]; + return { + ...scale, + scale: scale.scale.copy(), // 存在相同字段映射不同通道的情况 + }; } - - /** - * @example - * 'w*h' => ['w', 'h'] - * 'w' => ['w'] - */ - private parseFields(field: string[] | string): string[] { - if (Array.isArray(field)) { - return field; - } - if (isString(field)) { - return field.split('*'); - } - return [field]; - } - private mapping( attributes: { [attributeName: string]: ILayerStyleAttribute; @@ -93,11 +85,13 @@ export default class DataEncodePlugin implements ILayerPlugin { id: record._id, coordinates: record.coordinates, }; - // TODO 数据过滤 Object.keys(attributes).forEach((attributeName: string) => { const attribute = attributes[attributeName]; + // const { type } = attribute; + // if (type === StyleScaleType.CONSTANT) { + // return; + // } let values = this.getAttrValue(attribute, record); - if (attributeName === 'color') { values = values.map((c: unknown) => { return rgb2arr(c as string); @@ -117,8 +111,11 @@ export default class DataEncodePlugin implements ILayerPlugin { const scales = attribute.scales || []; const params: unknown[] = []; - scales.forEach(({ field }) => { - if (record[field]) { + scales.forEach((scale) => { + const { field, type } = scale; + if (type === StyleScaleType.CONSTANT) { + params.push(scale.field); + } else { params.push(record[field]); } }); diff --git a/packages/layers/src/plugins/DataSourcePlugin.ts b/packages/layers/src/plugins/DataSourcePlugin.ts new file mode 100644 index 0000000000..ed4aa268be --- /dev/null +++ b/packages/layers/src/plugins/DataSourcePlugin.ts @@ -0,0 +1,21 @@ +import { + IGlobalConfigService, + ILayer, + ILayerPlugin, + ILayerStyleAttribute, + IParseDataItem, + IStyleScale, + lazyInject, + StyleScaleType, + TYPES, +} from '@l7/core'; +import { ISourceCFG } from '@l7/core'; +import Source from '@l7/source'; +export default class DataSourcePlugin implements ILayerPlugin { + public apply(layer: ILayer) { + layer.hooks.init.tap('DataSourcePlugin', () => { + const { data, options } = layer.sourceOption; + layer.setSource(new Source(data, options)); + }); + } +} diff --git a/packages/layers/src/point/buffers/ExtrudeBuffer.ts b/packages/layers/src/point/buffers/ExtrudeBuffer.ts new file mode 100644 index 0000000000..a3b168f3a3 --- /dev/null +++ b/packages/layers/src/point/buffers/ExtrudeBuffer.ts @@ -0,0 +1,79 @@ +import BaseBuffer, { + IBufferInfo, + IEncodeFeature, + Position, +} from '../../core/BaseBuffer'; +import extrudePolygon, { IExtrudeGeomety } from '../shape/extrude'; +import { geometryShape, ShapeType2D, ShapeType3D } from '../shape/Path'; +interface IGeometryCache { + [key: string]: IExtrudeGeomety; +} +export default class ExtrudeBuffer extends BaseBuffer { + private indexOffset: number = 0; + private verticesOffset: number = 0; + private geometryCache: IGeometryCache; + public buildFeatures() { + const layerData = this.data as IEncodeFeature[]; + layerData.forEach((feature: IEncodeFeature) => { + this.calculateFill(feature); + }); + } + + protected calculateFeatures() { + const layerData = this.data as IEncodeFeature[]; + this.geometryCache = {}; + this.verticesOffset = 0; + this.indexOffset = 0; + layerData.forEach((feature: IEncodeFeature) => { + const { shape } = feature; + const { positions, index } = this.getGeometry(shape as ShapeType3D); + this.verticesCount += positions.length / 3; + this.indexCount += index.length; + }); + } + protected initAttributes() { + super.initAttributes(); + this.attributes.miters = new Float32Array(this.verticesCount * 3); + this.attributes.normals = new Float32Array(this.verticesCount * 3); + this.attributes.sizes = new Float32Array(this.verticesCount * 3); + } + private calculateFill(feature: IEncodeFeature) { + const { coordinates, shape } = feature; + const instanceGeometry = this.getGeometry(shape as ShapeType3D); + const numPoint = instanceGeometry.positions.length / 3; + feature.bufferInfo = { + verticesOffset: this.verticesOffset, + indexOffset: this.indexOffset, + dimensions: 3, + }; + this.encodeArray(feature, numPoint); + this.attributes.miters.set( + instanceGeometry.positions, + this.verticesOffset * 3, + ); + const indexArray = instanceGeometry.index.map((v) => { + return v + this.verticesOffset; + }); + this.indexArray.set(indexArray, this.indexOffset); + const position: number[] = []; + for (let i = 0; i < numPoint; i++) { + const coor = coordinates as Position; + position.push(coor[0], coor[1], coor[2] || 0); + } + this.attributes.positions.set(position, this.verticesOffset * 3); + this.verticesOffset += numPoint; + this.indexOffset += indexArray.length; + } + + private getGeometry(shape: ShapeType3D): IExtrudeGeomety { + if (this.geometryCache && this.geometryCache[shape]) { + return this.geometryCache[shape]; + } + const path = geometryShape[shape] + ? geometryShape[shape]() + : geometryShape.cylinder(); + const geometry = extrudePolygon([path]); + this.geometryCache[shape] = geometry; + return geometry; + } +} diff --git a/packages/layers/src/point/buffers/ImageBuffer.ts b/packages/layers/src/point/buffers/ImageBuffer.ts new file mode 100644 index 0000000000..510a77bdd8 --- /dev/null +++ b/packages/layers/src/point/buffers/ImageBuffer.ts @@ -0,0 +1,22 @@ +import BaseBuffer, { IEncodeFeature, Position } from '../../core/BaseBuffer'; +export default class ImageBuffer extends BaseBuffer { + protected calculateFeatures() { + const layerData = this.data as IEncodeFeature[]; + this.verticesCount = layerData.length; + this.indexCount = layerData.length; + } + protected buildFeatures() { + const layerData = this.data as IEncodeFeature[]; + this.attributes.uv = new Float32Array(this.verticesCount * 2); + layerData.forEach((item: IEncodeFeature, index: number) => { + const { color = [0, 0, 0, 0], size, id, shape, coordinates } = item; + const { x, y } = this.iconMap[shape as string] || { x: 0, y: 0 }; + const coor = coordinates as Position; + this.attributes.positions.set(coor, index * 3); + this.attributes.colors.set(color, index * 4); + this.attributes.pickingIds.set([id as number], index); + this.attributes.sizes.set([size as number], index); // + this.attributes.uv.set([x, y], index * 2); + }); + } +} diff --git a/packages/layers/src/point/index.ts b/packages/layers/src/point/index.ts index 45aa40db2a..0a7c4e3a64 100644 --- a/packages/layers/src/point/index.ts +++ b/packages/layers/src/point/index.ts @@ -1,5 +1,6 @@ import { gl, + ILayer, IRendererService, IShaderModuleService, lazyInject, @@ -50,15 +51,15 @@ export default class PointLayer extends BaseLayer { private pointFeatures: IPointFeature[] = []; - public style(options: Partial) { - // this.layerStyleService.update(options); - // this.styleOptions = { - // ...this.styleOptions, - // ...options, - // }; - } + // public style(options: Partial) { + // // this.layerStyleService.update(options); + // // this.styleOptions = { + // // ...this.styleOptions, + // // ...options, + // // }; + // } - public render() { + public render(): ILayer { this.models.forEach((model) => model.draw({ uniforms: { diff --git a/packages/layers/src/point/point.ts b/packages/layers/src/point/point.ts new file mode 100644 index 0000000000..e7f7e10e9a --- /dev/null +++ b/packages/layers/src/point/point.ts @@ -0,0 +1,131 @@ +import { + gl, + IIconService, + IRendererService, + IShaderModuleService, + lazyInject, + TYPES, +} from '@l7/core'; +import BaseLayer from '../core/BaseLayer'; +import ExtrudeBuffer from './buffers/ExtrudeBuffer'; +import ImageBuffer from './buffers/ImageBuffer'; +import extrude_frag from './shaders/extrude_frag.glsl'; +import extrude_vert from './shaders/extrude_vert.glsl'; +import image_frag from './shaders/image_frag.glsl'; +import image_vert from './shaders/image_vert.glsl'; + +export default class PointLayer extends BaseLayer { + public name: string = 'PointLayer'; + + @lazyInject(TYPES.IShaderModuleService) + private readonly shaderModule: IShaderModuleService; + + @lazyInject(TYPES.IRendererService) + private readonly renderer: IRendererService; + + + protected renderModels() { + this.models.forEach((model) => + model.draw({ + uniforms: { + u_ModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + }, + }), + ); + return this; + } + + protected buildModels(): void { + this.shaderModule.registerModule('point', { + vs: extrude_vert, + fs: extrude_frag, + }); + this.shaderModule.registerModule('pointImage', { + vs: image_vert, + fs: image_frag, + }); + + this.models = []; + const { vs, fs, uniforms } = this.shaderModule.getModule('pointImage'); + // const buffer = new ExtrudeBuffer({ + // data: this.getEncodedData(), + // }); + // buffer.computeVertexNormals('miters', false); + const { + createAttribute, + createBuffer, + createElements, + createTexture2D, + createModel, + } = this.renderer; + const buffer = new ImageBuffer({ + data: this.getEncodedData(), + iconMap: this.iconService.getIconMap(), + }); + this.models.push( + createModel({ + attributes: { + a_Position: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.positions, + type: gl.FLOAT, + }), + size: 3, + }), + a_normal: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.normals, + type: gl.FLOAT, + }), + size: 3, + }), + a_color: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.colors, + type: gl.FLOAT, + }), + size: 4, + }), + a_size: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.sizes, + type: gl.FLOAT, + }), + size: 1, + }), + a_uv: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.uv, + type: gl.FLOAT, + }), + size: 2, + }), + // a_shape: createAttribute({ + // buffer: createBuffer({ + // data: buffer.attributes.miters, + // type: gl.FLOAT, + // }), + // size: 3, + // }), + }, + uniforms: { + ...uniforms, + u_opacity: this.styleOption.opacity as number, + u_texture: createTexture2D({ + data: this.iconService.getCanvas(), + width: 1024, + height: this.iconService.canvasHeight, + }), + }, + fs, + vs, + primitive: gl.POINTS, + count: buffer.verticesCount, + // elements: createElements({ + // data: buffer.indexArray, + // type: gl.UNSIGNED_INT, + // }), + }), + ); + } +} diff --git a/packages/layers/src/point/shaders/extrude_frag.glsl b/packages/layers/src/point/shaders/extrude_frag.glsl new file mode 100644 index 0000000000..6f26ac6083 --- /dev/null +++ b/packages/layers/src/point/shaders/extrude_frag.glsl @@ -0,0 +1,6 @@ +varying vec4 v_color; +uniform float u_opacity: 1.0; +void main() { + gl_FragColor = v_color; + gl_FragColor.a *= u_opacity; +} \ No newline at end of file diff --git a/packages/layers/src/point/shaders/extrude_vert.glsl b/packages/layers/src/point/shaders/extrude_vert.glsl new file mode 100644 index 0000000000..32432f1938 --- /dev/null +++ b/packages/layers/src/point/shaders/extrude_vert.glsl @@ -0,0 +1,19 @@ +precision highp float; +attribute vec3 a_Position; +attribute vec4 a_color; +attribute vec3 a_size; +attribute vec3 a_shape; +attribute vec3 a_normal; + +uniform mat4 u_ModelMatrix; +varying vec4 v_color; + +#pragma include "projection" +void main() { + vec3 size = a_size * a_shape; + v_color = vec4(a_normal,1.0); + vec2 offset = project_pixel(size.xy); + vec4 project_pos = project_position(vec4(a_Position.xy, 0, 1.0)); + gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy + offset, size.z, 1.0)); + +} \ No newline at end of file diff --git a/packages/layers/src/point/shaders/image_frag.glsl b/packages/layers/src/point/shaders/image_frag.glsl new file mode 100644 index 0000000000..c492ed2a4c --- /dev/null +++ b/packages/layers/src/point/shaders/image_frag.glsl @@ -0,0 +1,14 @@ +uniform sampler2D u_texture; +varying vec4 v_color; +varying vec2 v_uv; +void main(){ + vec2 pos= v_uv + gl_PointCoord / vec2(1024.,128.)*64.; + pos.y= 1.- pos.y; + vec4 textureColor=texture2D(u_texture,pos); + if(v_color == vec4(0.)){ + gl_FragColor= textureColor; + }else { + gl_FragColor= step(0.01, textureColor.x) * v_color; + } + return; +} \ No newline at end of file diff --git a/packages/layers/src/point/shaders/image_vert.glsl b/packages/layers/src/point/shaders/image_vert.glsl new file mode 100644 index 0000000000..bb0d424b45 --- /dev/null +++ b/packages/layers/src/point/shaders/image_vert.glsl @@ -0,0 +1,18 @@ +precision highp float; +attribute vec3 a_Position; +attribute vec4 a_color; +attribute vec2 a_uv; +attribute float a_size; +attribute float a_shape; +varying vec4 v_color; +varying vec2 v_uv; +uniform mat4 u_ModelMatrix; +#pragma include "projection" +void main() { + v_color = a_color; + v_uv = a_uv; + vec4 project_pos = project_position(vec4(a_Position, 1.0)); + gl_Position = project_common_position_to_clipspace(vec4(project_pos.xyz, 1.0)); + gl_PointSize = a_size; + +} \ No newline at end of file diff --git a/packages/layers/src/point/shape/Path.ts b/packages/layers/src/point/shape/Path.ts new file mode 100644 index 0000000000..1e12eb5709 --- /dev/null +++ b/packages/layers/src/point/shape/Path.ts @@ -0,0 +1,65 @@ +type IPosition = [number, number, number]; +export type IPath = IPosition[]; +export enum ShapeType3D { + CYLINDER = 'cylinder', + SQUARECOLUMN = 'squareColumn', + TRIANGLECOLUMN = 'triangleColumn', + HEXAGONCOLUMN = 'hexagonColumn', + PENTAGONCOLUMN = 'pentagonColumn', +} +export enum ShapeType2D { + CIRCLE = 'circle', + SQUARE = 'square', + TRIANGLE = 'triangle', + HEXAGON = 'hexagon', + PENTAGON = 'pentagon', +} + +/** + * 生成规则多边形顶点个数 + * @param pointCount 顶点个数 3 => 三角形 + * @param start 顶点起始角度 调整图形的方向 + */ +export function polygonPath(pointCount: number, start: number = 0): IPath { + const step = (Math.PI * 2) / pointCount; + const line = []; + for (let i = 0; i < pointCount; i++) { + line.push(step * i + (start * Math.PI) / 12); + } + const path: IPath = line.map((t) => { + const x = Math.sin(t + Math.PI / 4); + const y = Math.cos(t + Math.PI / 4); + return [x, y, 0]; + }); + // path.push(path[0]); + return path; +} + +export function circle(): IPath { + return polygonPath(30); +} +export function square(): IPath { + return polygonPath(4); +} +export function triangle(): IPath { + return polygonPath(3); +} +export function hexagon(): IPath { + return polygonPath(6); +} +export function pentagon(): IPath { + return polygonPath(5); +} + +export const geometryShape = { + [ShapeType2D.CIRCLE]: circle, + [ShapeType2D.HEXAGON]: hexagon, + [ShapeType2D.TRIANGLE]: triangle, + [ShapeType2D.SQUARE]: square, + [ShapeType2D.PENTAGON]: pentagon, + [ShapeType3D.CYLINDER]: circle, + [ShapeType3D.HEXAGONCOLUMN]: hexagon, + [ShapeType3D.TRIANGLECOLUMN]: triangle, + [ShapeType3D.SQUARECOLUMN]: square, + [ShapeType3D.PENTAGONCOLUMN]: pentagon, +}; diff --git a/packages/layers/src/point/shape/extrude.ts b/packages/layers/src/point/shape/extrude.ts new file mode 100644 index 0000000000..717d585609 --- /dev/null +++ b/packages/layers/src/point/shape/extrude.ts @@ -0,0 +1,74 @@ +import earcut from 'earcut'; +import { IPath } from './Path'; +export interface IExtrudeGeomety { + positions: number[]; + index: number[]; +} +/** + * 拉伸多边形顶点,返回拉伸后的顶点信息 + * @param paths 路径数据组 + * @param extrude 是否拉伸 + */ +export default function extrudePolygon(path: IPath[]): IExtrudeGeomety { + const p1 = path[0][0]; + const p2 = path[0][path[0].length - 1]; + if (p1[0] === p2[0] && p1[1] === p2[1]) { + path[0] = path[0].slice(0, path[0].length - 1); + } + const n = path[0].length; + const flattengeo = earcut.flatten(path); + const positions = []; + const indexArray = []; + const normals = []; + // 设置顶部z值 + for (let j = 0; j < flattengeo.vertices.length / 3; j++) { + flattengeo.vertices[j * 3 + 2] = 1; + normals.push(0, 0, 1); + } + positions.push(...flattengeo.vertices); + const triangles = earcut( + flattengeo.vertices, + flattengeo.holes, + flattengeo.dimensions, + ); + indexArray.push(...triangles); + for (let i = 0; i < n; i++) { + const prePoint = flattengeo.vertices.slice(i * 3, i * 3 + 3); + let nextPoint = flattengeo.vertices.slice(i * 3 + 3, i * 3 + 6); + if (nextPoint.length === 0) { + nextPoint = flattengeo.vertices.slice(0, 3); + } + const indexOffset = positions.length / 3; + positions.push( + prePoint[0], + prePoint[1], + 1, + nextPoint[0], + nextPoint[1], + 1, + prePoint[0], + prePoint[1], + 0, + nextPoint[0], + nextPoint[1], + 0, + ); + indexArray.push(...[1, 2, 0, 3, 2, 1].map((v) => v + indexOffset)); + } + return { + positions, + index: indexArray, + }; +} +export function fillPolygon(points: IPath[]) { + const flattengeo = earcut.flatten(points); + const triangles = earcut( + flattengeo.vertices, + flattengeo.holes, + flattengeo.dimensions, + ); + return { + positions: flattengeo.vertices, + index: triangles, + }; +} diff --git a/packages/layers/src/polygon/buffers/ExtrudeBuffer.ts b/packages/layers/src/polygon/buffers/ExtrudeBuffer.ts new file mode 100644 index 0000000000..9b0697d590 --- /dev/null +++ b/packages/layers/src/polygon/buffers/ExtrudeBuffer.ts @@ -0,0 +1,137 @@ +import earcut from 'earcut'; +import BufferBase, { + IBufferInfo, + IEncodeFeature, + Position, +} from '../../core/BaseBuffer'; +export default class ExtrudeBuffer extends BufferBase { + public buildFeatures() { + const layerData = this.data as IEncodeFeature[]; + layerData.forEach((feature: IEncodeFeature) => { + this.calculateTop(feature); + this.calculateWall(feature); + delete feature.bufferInfo; + }); + } + + public calculateFeatures() { + const layerData = this.data as IEncodeFeature[]; + // 计算长 + layerData.forEach((feature: IEncodeFeature) => { + const coordinates = feature.coordinates as Position[][]; + const flattengeo = earcut.flatten(coordinates); + const n = this.checkIsClosed(coordinates) + ? coordinates[0].length - 1 + : coordinates[0].length; + const { vertices, dimensions, holes } = flattengeo; + const indexArray = earcut(vertices, holes, dimensions).map( + (v) => this.verticesCount + v, + ); + const bufferInfo: IBufferInfo = { + dimensions, + vertices, + indexArray, + verticesOffset: this.verticesCount + 0, + indexOffset: this.indexCount + 0, + faceNum: n, + }; + this.indexCount += indexArray.length + n * 6; + this.verticesCount += vertices.length / dimensions + n * 4; + feature.bufferInfo = bufferInfo; + }); + } + protected calculateWall(feature: IEncodeFeature) { + const size = feature.size || 0; + const bufferInfo = feature.bufferInfo as IBufferInfo; + const { + vertices, + indexOffset, + verticesOffset, + faceNum, + dimensions, + } = bufferInfo; + this.encodeArray(feature, faceNum * 4); + for (let i = 0; i < faceNum; i++) { + const prePoint = vertices.slice(i * dimensions, (i + 1) * dimensions); + const nextPoint = vertices.slice( + (i + 1) * dimensions, + (i + 2) * dimensions, + ); + this.calculateExtrudeFace( + prePoint, + nextPoint, + verticesOffset + i * 4, + indexOffset + i * 6, + size as number, + ); + bufferInfo.verticesOffset += 4; + bufferInfo.indexOffset += 6; + feature.bufferInfo = bufferInfo; + } + } + private calculateTop(feature: IEncodeFeature) { + const size = feature.size || 1; + const bufferInfo = feature.bufferInfo as IBufferInfo; + const { + indexArray, + vertices, + indexOffset, + verticesOffset, + dimensions, + } = bufferInfo; + const pointCount = vertices.length / dimensions; + this.encodeArray(feature, vertices.length / dimensions); + // 添加顶点 + for (let i = 0; i < pointCount; i++) { + this.attributes.positions.set( + [vertices[i * dimensions], vertices[i * dimensions + 1], size], + (verticesOffset + i) * 3, + ); + // 顶部文理坐标计算 + // if (this.uv) { + // // TODO 用过BBox计算纹理坐标 + // this.attributes.uv.set([-1, -1], (verticesOffset + i) * 2); + // } + } + bufferInfo.verticesOffset += pointCount; + // 添加顶点索引 + this.indexArray.set(indexArray, indexOffset); // 顶部坐标 + bufferInfo.indexOffset += indexArray.length; + feature.bufferInfo = bufferInfo; + } + private calculateExtrudeFace( + prePoint: number[], + nextPoint: number[], + positionOffset: number, + indexOffset: number | undefined, + size: number, + ) { + this.attributes.positions.set( + [ + prePoint[0], + prePoint[1], + size, + nextPoint[0], + nextPoint[1], + size, + prePoint[0], + prePoint[1], + 0, + nextPoint[0], + nextPoint[1], + 0, + ], + positionOffset * 3, + ); + const indexArray = [1, 2, 0, 3, 2, 1].map((v) => { + return v + positionOffset; + }); + // if (this.uv) { + // this.attributes.uv.set( + // [0.1, 0, 0, 0, 0.1, size / 2000, 0, size / 2000], + // positionOffset * 2, + // ); + // } + this.indexArray.set(indexArray, indexOffset); + } +} diff --git a/packages/layers/src/polygon/buffers/FillBuffer.ts b/packages/layers/src/polygon/buffers/FillBuffer.ts index 3e8df52b15..c1e03a1459 100644 --- a/packages/layers/src/polygon/buffers/FillBuffer.ts +++ b/packages/layers/src/polygon/buffers/FillBuffer.ts @@ -1,5 +1,9 @@ import earcut from 'earcut'; -import BufferBase, { IBufferInfo, IEncodeFeature } from '../../core/BaseBuffer'; +import BufferBase, { + IBufferInfo, + IEncodeFeature, + Position, +} from '../../core/BaseBuffer'; export default class FillBuffer extends BufferBase { protected buildFeatures() { const layerData = this.data as IEncodeFeature[]; @@ -14,7 +18,7 @@ export default class FillBuffer extends BufferBase { // 计算长 layerData.forEach((feature: IEncodeFeature) => { const { coordinates } = feature; - const flattengeo = earcut.flatten(coordinates); + const flattengeo = earcut.flatten(coordinates as Position[][]); const { vertices, dimensions, holes } = flattengeo; const indexArray = earcut(vertices, holes, dimensions).map( (v) => this.verticesCount + v, @@ -33,13 +37,14 @@ export default class FillBuffer extends BufferBase { } private calculateFill(feature: IEncodeFeature) { + const bufferInfo = feature.bufferInfo as IBufferInfo; const { indexArray, vertices, indexOffset, verticesOffset, dimensions = 3, - } = feature.bufferInfo; + } = bufferInfo; const pointCount = vertices.length / dimensions; this.encodeArray(feature, pointCount); // 添加顶点 @@ -48,15 +53,16 @@ export default class FillBuffer extends BufferBase { [vertices[i * dimensions], vertices[i * dimensions + 1], 0], (verticesOffset + i) * 3, ); - if (this.uv) { - // TODO 用过BBox计算纹理坐标 - this.attributes.uv.set( - [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0], - (verticesOffset + i) * 3, - ); - } + // if (this.uv) { + // // TODO 用过BBox计算纹理坐标 + // this.attributes.uv.set( + // [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0], + // (verticesOffset + i) * 3, + // ); + // } } - feature.bufferInfo.verticesOffset += pointCount; + bufferInfo.verticesOffset += pointCount; + feature.bufferInfo = bufferInfo; // 添加顶点索引 this.indexArray.set(indexArray, indexOffset); // 顶部坐标 } diff --git a/packages/layers/src/polygon/buffers/extrude_buffer.ts b/packages/layers/src/polygon/buffers/extrude_buffer.ts deleted file mode 100644 index 8fd8b036bc..0000000000 --- a/packages/layers/src/polygon/buffers/extrude_buffer.ts +++ /dev/null @@ -1,66 +0,0 @@ -// import earcut from 'earcut'; -// import BufferBase, { IBufferInfo, IEncodeFeature } from '../../core/buffer'; -// export default class ExtrudeButffer extends BufferBase { -// public _buildFeatures() { -// const layerData = this.get('data'); -// layerData.forEach((feature: IEncodeFeature) => { -// this.calculateTop(feature); -// this.calculateWall(feature); -// delete feature.bufferInfo; -// }); -// } - -// public _calculateFeatures() { -// const layerData = this.get('data'); -// // 计算长 -// layerData.forEach((feature: IEncodeFeature) => { -// const { coordinates } = feature; -// const flattengeo = earcut.flatten(coordinates); -// const n = this.checkIsClosed(coordinates) -// ? coordinates[0].length - 1 -// : coordinates[0].length; -// const { vertices, dimensions, holes } = flattengeo; -// const indexArray = earcut(vertices, holes, dimensions).map( -// (v) => this.verticesCount + v, -// ); -// const bufferInfo: IBufferInfo = { -// dimensions, -// vertices, -// indexArray, -// verticesOffset: this.verticesCount + 0, -// indexOffset: this.indexCount + 0, -// faceNum: n, -// }; -// this.indexCount += indexArray.length + n * 6; -// this.verticesCount += vertices.length / dimensions + n * 4; -// feature.bufferInfo = bufferInfo; -// }); -// } -// private calculateTop(feature: IEncodeFeature) { -// const size = feature.size; -// const { -// indexArray, -// vertices, -// indexOffset, -// verticesOffset, -// dimensions, -// } = feature.bufferInfo; -// const pointCount = vertices.length / dimensions; -// this.encodeArray(feature, dimensions); -// // 添加顶点 -// for (let i = 0; i < pointCount; i++) { -// this.attributes.positions.set( -// [vertices[i * 3], vertices[i * 3 + 1], size], -// (verticesOffset + i) * 3, -// ); -// // 顶部文理坐标计算 -// if (this.get('uv')) { -// // TODO 用过BBox计算纹理坐标 -// this.attributes.uv.set([-1, -1], (verticesOffset + i) * 2); -// } -// } -// feature.bufferInfo.verticesOffset += pointCount; -// // 添加顶点索引 -// this.indexArray.set(indexArray, indexOffset); // 顶部坐标 -// } -// } diff --git a/packages/layers/src/polygon/index.ts b/packages/layers/src/polygon/index.ts index b2f05d7a7e..38f355aacf 100644 --- a/packages/layers/src/polygon/index.ts +++ b/packages/layers/src/polygon/index.ts @@ -6,6 +6,7 @@ import { TYPES, } from '@l7/core'; import BaseLayer from '../core/BaseLayer'; +import ExtrudeBuffer from './buffers/ExtrudeBuffer'; import FillBuffer from './buffers/FillBuffer'; import polygon_frag from './shaders/polygon_frag.glsl'; import polygon_vert from './shaders/polygon_vert.glsl'; @@ -38,10 +39,13 @@ export default class PolygonLayer extends BaseLayer { this.models = []; const { vs, fs, uniforms } = this.shaderModule.getModule('polygon'); + // const buffer = new ExtrudeBuffer({ + // data: this.getEncodedData(), + // }); + // buffer.computeVertexNormals(); const buffer = new FillBuffer({ data: this.getEncodedData(), }); - const { createAttribute, createBuffer, @@ -59,6 +63,13 @@ export default class PolygonLayer extends BaseLayer { }), size: 3, }), + a_normal: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.normals, + type: gl.FLOAT, + }), + size: 3, + }), a_color: createAttribute({ buffer: createBuffer({ data: buffer.attributes.colors, @@ -67,7 +78,10 @@ export default class PolygonLayer extends BaseLayer { size: 4, }), }, - uniforms, + uniforms: { + ...uniforms, + u_opacity: this.styleOption.opacity as number, + }, fs, vs, count: buffer.indexArray.length, diff --git a/packages/layers/src/polygon/shaders/polygon_frag.glsl b/packages/layers/src/polygon/shaders/polygon_frag.glsl index 59f63f536c..6f26ac6083 100644 --- a/packages/layers/src/polygon/shaders/polygon_frag.glsl +++ b/packages/layers/src/polygon/shaders/polygon_frag.glsl @@ -1,4 +1,6 @@ varying vec4 v_color; +uniform float u_opacity: 1.0; void main() { gl_FragColor = v_color; + gl_FragColor.a *= u_opacity; } \ No newline at end of file diff --git a/packages/layers/src/polygon/shaders/polygon_vert.glsl b/packages/layers/src/polygon/shaders/polygon_vert.glsl index 05efd8436c..b6a1610cb9 100644 --- a/packages/layers/src/polygon/shaders/polygon_vert.glsl +++ b/packages/layers/src/polygon/shaders/polygon_vert.glsl @@ -1,6 +1,6 @@ attribute vec4 a_color; attribute vec3 a_Position; - +attribute vec3 a_normal; uniform mat4 u_ModelMatrix; varying vec4 v_color; diff --git a/packages/layers/src/raster/buffers/ImageBuffer.ts b/packages/layers/src/raster/buffers/ImageBuffer.ts new file mode 100644 index 0000000000..66f3398e2d --- /dev/null +++ b/packages/layers/src/raster/buffers/ImageBuffer.ts @@ -0,0 +1,33 @@ +import BaseBuffer, { IEncodeFeature, Position } from '../../core/BaseBuffer'; +interface IImageFeature extends IEncodeFeature { + images: any[]; +} +export default class ImageBuffer extends BaseBuffer { + protected calculateFeatures() { + this.verticesCount = 6; + this.indexCount = 6; + } + protected buildFeatures() { + this.attributes.uv = new Float32Array(this.verticesCount * 2); + const layerData = this.data as IImageFeature[]; + const coordinates = layerData[0].coordinates as Position[]; + const positions: number[] = [ + ...coordinates[0], + 0, + coordinates[1][0], + coordinates[0][1], + 0, + ...coordinates[1], + 0, + ...coordinates[0], + 0, + ...coordinates[1], + 0, + coordinates[0][0], + coordinates[1][1], + 0, + ]; + this.attributes.positions.set(positions, 0); + this.attributes.uv.set([0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0], 0); + } +} diff --git a/packages/layers/src/raster/index.ts b/packages/layers/src/raster/index.ts new file mode 100644 index 0000000000..1b4366dc4f --- /dev/null +++ b/packages/layers/src/raster/index.ts @@ -0,0 +1,87 @@ +import { + gl, + IRendererService, + IShaderModuleService, + ITexture2D, + lazyInject, + TYPES, +} from '@l7/core'; +import BaseLayer from '../core/BaseLayer'; +import ImageBuffer from './buffers/ImageBuffer'; +import image_frag from './shaders/image_frag.glsl'; +import image_vert from './shaders/image_vert.glsl'; +export default class ImageLayer extends BaseLayer { + public name: string = 'imageLayer'; + @lazyInject(TYPES.IShaderModuleService) + private readonly shaderModule: IShaderModuleService; + + @lazyInject(TYPES.IRendererService) + private readonly renderer: IRendererService; + + protected renderModels() { + this.models.forEach((model) => + model.draw({ + uniforms: { + u_ModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + }, + }), + ); + return this; + } + protected buildModels() { + const { + createAttribute, + createBuffer, + createElements, + createTexture2D, + createModel, + } = this.renderer; + this.shaderModule.registerModule('image', { + vs: image_vert, + fs: image_frag, + }); + + this.models = []; + const { vs, fs, uniforms } = this.shaderModule.getModule('image'); + const source = this.getSource(); + // const imageData = await source.data.images; + const buffer = new ImageBuffer({ + data: this.getEncodedData(), + }); + source.data.images.then((imageData: HTMLImageElement[]) => { + const texture: ITexture2D = createTexture2D({ + data: imageData[0], + width: imageData[0].width, + height: imageData[0].height, + }); + this.models.push( + createModel({ + attributes: { + a_Position: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.positions, + type: gl.FLOAT, + }), + size: 3, + }), + a_uv: createAttribute({ + buffer: createBuffer({ + data: buffer.attributes.uv, + type: gl.FLOAT, + }), + size: 2, + }), + }, + uniforms: { + ...uniforms, + u_texture: texture, + u_opacity: 1.0, + }, + fs, + vs, + count: buffer.verticesCount, + }), + ); + }); + } +} diff --git a/packages/layers/src/raster/shaders/image_frag.glsl b/packages/layers/src/raster/shaders/image_frag.glsl new file mode 100644 index 0000000000..8d3b5683b4 --- /dev/null +++ b/packages/layers/src/raster/shaders/image_frag.glsl @@ -0,0 +1,8 @@ +precision mediump float; +uniform float u_opacity: 1.0; +uniform sampler2D u_texture; +varying vec2 v_texCoord; +void main() { + vec4 color = texture2D(u_texture,vec2(v_texCoord.x,v_texCoord.y)); + gl_FragColor = color; +} \ No newline at end of file diff --git a/packages/layers/src/raster/shaders/image_vert.glsl b/packages/layers/src/raster/shaders/image_vert.glsl new file mode 100644 index 0000000000..f0b98f4a57 --- /dev/null +++ b/packages/layers/src/raster/shaders/image_vert.glsl @@ -0,0 +1,12 @@ +precision highp float; +uniform mat4 u_ModelMatrix; +attribute vec3 a_Position; +attribute vec2 a_uv; +varying vec2 v_texCoord; + +#pragma include "projection" +void main() { + v_texCoord = a_uv; + vec4 project_pos = project_position(vec4(a_Position, 1.0)); + gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy,0., 1.0)); +} \ No newline at end of file diff --git a/packages/layers/src/utils/color.ts b/packages/layers/src/utils/color.ts index 12ed54742f..27af9f87d3 100644 --- a/packages/layers/src/utils/color.ts +++ b/packages/layers/src/utils/color.ts @@ -1,11 +1,12 @@ +import * as d3 from 'd3-color'; export function rgb2arr(str: string) { - const arr = []; - if (str.length === 4) { - str = `#${str[1]}${str[1]}${str[2]}${str[2]}${str[3]}${str[3]}`; + const color = d3.color(str) as d3.RGBColor; + const arr = [0, 0, 0, 0]; + if (color != null) { + arr[0] = color.r / 255; + arr[1] = color.g / 255; + arr[2] = color.b / 255; + arr[3] = color.opacity; } - arr.push(parseInt(str.substr(1, 2), 16) / 255); - arr.push(parseInt(str.substr(3, 2), 16) / 255); - arr.push(parseInt(str.substr(5, 2), 16) / 255); - arr.push(1.0); return arr; } diff --git a/packages/layers/src/utils/polylineNormal.ts b/packages/layers/src/utils/polylineNormal.ts new file mode 100644 index 0000000000..c49892a52a --- /dev/null +++ b/packages/layers/src/utils/polylineNormal.ts @@ -0,0 +1,221 @@ +import { aProjectFlat, lngLatToMeters, Point } from '@l7/utils'; +import { vec2 } from 'gl-matrix'; +export function computeMiter( + tangent: vec2, + miter: vec2, + lineA: vec2, + lineB: vec2, + halfThick: number, +) { + vec2.add(tangent, lineA, lineB); + vec2.normalize(tangent, tangent); + miter = vec2.fromValues(-tangent[1], tangent[0]); + const tmp = vec2.fromValues(-lineA[1], lineA[0]); + return halfThick / vec2.dot(miter, tmp); +} +export function computeNormal(out: vec2, dir: vec2) { + return vec2.set(out, -dir[1], dir[0]); +} +export function direction(out: vec2, a: vec2, b: vec2) { + const a1 = aProjectFlat([a[0], a[1]]); + const b1 = aProjectFlat([b[0], b[1]]); + vec2.sub(out, a1, b1); + vec2.normalize(out, out); + return out; +} +function extrusions( + positions: number[], + out: number[], + miters: number[], + point: vec2, + normal: vec2, + scale: number, +) { + addNext(out, miters, normal, -scale); + addNext(out, miters, normal, scale); + positions.push(point[0], point[1], 0); + positions.push(point[0], point[1], 0); +} + +function addNext( + out: number[], + miters: number[], + normal: vec2, + length: number, +) { + out.push(normal[0], normal[1], 0); + miters.push(length); +} + +function lineSegmentDistance(end: vec2, start: vec2) { + const dx = start[0] - end[0]; + const dy = start[1] - end[1]; + // const dz = start[2] - end[2]; + return Math.sqrt(dx * dx + dy * dy); +} + +function isPointEqual(a: vec2, b: vec2) { + return a[0] === b[0] && a[1] === b[1]; +} + +export default function( + points: number[][], + closed: boolean, + indexOffset: number, +) { + const lineA = vec2.fromValues(0, 0); + const lineB = vec2.fromValues(0, 0); + const tangent = vec2.fromValues(0, 0); + const miter: vec2 = vec2.create(); + let started = false; + let lineNormal = null; + const tmp = vec2.create(); + let count = indexOffset || 0; + const miterLimit = 3; + + const out: number[] = []; + const attrPos: number[] = []; + const attrIndex: number[] = []; + const miters: number[] = []; + const attrDistance = [0, 0]; + if (closed) { + points = points.slice(); + points.push(points[0]); + } + + const total = points.length; + + for (let i = 1; i < total; i++) { + const index = count; + const last = vec2.fromValues(points[i - 1][0], points[i - 1][1]); + const cur = vec2.fromValues(points[i][0], points[i][1]); + let next = + i < points.length - 1 + ? vec2.fromValues(points[i + 1][0], points[i + 1][1]) + : null; + // 如果当前点和前一点相同,跳过 + if (isPointEqual(last, cur)) { + continue; + } + if (next) { + let nextIndex = i + 1; + // 找到不相同的下一点 + while (next && isPointEqual(cur, next)) { + next = + nextIndex < points.length - 1 + ? vec2.fromValues(points[++nextIndex][0], points[nextIndex][1]) + : null; + } + } + const lineDistance = lineSegmentDistance(cur, last); + const d = lineDistance + attrDistance[attrDistance.length - 1]; + direction(lineA, cur, last); + if (!lineNormal) { + lineNormal = vec2.create(); + computeNormal(lineNormal, lineA); + } + + if (!started) { + started = true; + extrusions(attrPos, out, miters, last, lineNormal, 1); + } + + attrIndex.push(index + 0, index + 2, index + 1); + + // no miter, simple segment + if (!next) { + // reset normal + computeNormal(lineNormal, lineA); + extrusions(attrPos, out, miters, cur, lineNormal, 1); + attrDistance.push(d, d); + attrIndex.push(index + 1, index + 2, index + 3); + count += 2; + } else { + // get unit dir of next line + direction(lineB, next, cur); + + // stores tangent & miter + let miterLen = computeMiter( + tangent, + vec2.fromValues(miter[0], miter[1]), + lineA, + lineB, + 1, + ); + + // get orientation + const flip = vec2.dot(tangent, lineNormal) < 0 ? -1 : 1; + const bevel = Math.abs(miterLen) > miterLimit; + + // 处理前后两条线段重合的情况,这种情况不需要使用任何接头(miter/bevel)。 + // 理论上这种情况下 miterLen = Infinity,本应通过 isFinite(miterLen) 判断, + // 但是 AMap 投影变换后丢失精度,只能通过一个阈值(1000)判断。 + + if (Math.abs(miterLen) > 1000) { + extrusions(attrPos, out, miters, cur, lineNormal, 1); + attrIndex.push(index + 1, index + 2, index + 3); + attrIndex.push(index + 2, index + 4, index + 3); + computeNormal(tmp, lineB); + vec2.copy(lineNormal, tmp); // store normal for next round + + extrusions(attrPos, out, miters, cur, lineNormal, 1); + attrDistance.push(d, d, d, d); + + // the miter is now the normal for our next join + count += 4; + continue; + } + + if (bevel) { + miterLen = miterLimit; + + // next two points in our first segment + extrusions(attrPos, out, miters, cur, lineNormal, 1); + + attrIndex.push(index + 1, index + 2, index + 3); + + // now add the bevel triangle + attrIndex.push( + ...(flip === 1 + ? [index + 2, index + 4, index + 5] + : [index + 4, index + 5, index + 3]), + ); + + computeNormal(tmp, lineB); + vec2.copy(lineNormal, tmp); // store normal for next round + + extrusions(attrPos, out, miters, cur, lineNormal, 1); + attrDistance.push(d, d, d, d); + + // the miter is now the normal for our next join + count += 4; + } else { + // next two points in our first segment + extrusions(attrPos, out, miters, cur, lineNormal, 1); + attrIndex.push(index + 1, index + 2, index + 3); + + // now add the miter triangles + addNext(out, miters, lineNormal, miterLen * -flip); + attrPos.push(cur[0], cur[1], 0); + attrIndex.push(index + 2, index + 4, index + 3); + attrIndex.push(index + 4, index + 5, index + 6); + computeNormal(tmp, lineB); + vec2.copy(lineNormal, tmp); // store normal for next round + + extrusions(attrPos, out, miters, cur, lineNormal, 1); + attrDistance.push(d, d, d, d, d); + + // the miter is now the normal for our next join + count += 5; + } + } + } + + return { + normals: out, + attrIndex, + attrPos, + attrDistance, + miters, + }; +} diff --git a/packages/renderer/src/regl/ReglModel.ts b/packages/renderer/src/regl/ReglModel.ts index 11c5b046c0..e688a19e4f 100644 --- a/packages/renderer/src/regl/ReglModel.ts +++ b/packages/renderer/src/regl/ReglModel.ts @@ -44,8 +44,8 @@ export default class ReglModel implements IModel { blend, stencil, cull, + instances, } = options; - const reglUniforms: { [key: string]: IUniform } = {}; if (uniforms) { this.uniforms = uniforms; @@ -60,7 +60,6 @@ export default class ReglModel implements IModel { Object.keys(attributes).forEach((name: string) => { reglAttributes[name] = (attributes[name] as ReglAttribute).get(); }); - const drawParams: regl.DrawConfig = { attributes: reglAttributes, frag: fs, @@ -70,6 +69,9 @@ export default class ReglModel implements IModel { primitiveMap[primitive === undefined ? gl.TRIANGLES : primitive], count, }; + if (instances) { + drawParams.instances = instances; + } if (elements) { drawParams.elements = (elements as ReglElements).get(); diff --git a/packages/renderer/src/regl/index.ts b/packages/renderer/src/regl/index.ts index a03f46baf4..8b1d617c03 100644 --- a/packages/renderer/src/regl/index.ts +++ b/packages/renderer/src/regl/index.ts @@ -56,6 +56,7 @@ export default class ReglRendererService implements IRendererService { 'EXT_SRGB', // baseColor emmisive 'OES_texture_float', // shadow map 'WEBGL_depth_texture', + 'angle_instanced_arrays', 'EXT_texture_filter_anisotropic', // VSM shadow map ], optionalExtensions: ['oes_texture_float_linear'], diff --git a/packages/scene/src/index.ts b/packages/scene/src/index.ts index 40ce761aec..08c6792fd9 100644 --- a/packages/scene/src/index.ts +++ b/packages/scene/src/index.ts @@ -1,5 +1,6 @@ import { container, + IImage, ILayer, IMapConfig, IMapService, @@ -73,10 +74,14 @@ class Scene { public render(): void { this.sceneService.render(); } - + public addImage(id: string, img: IImage) { + this.sceneService.addImage(id, img); + } public destroy() { this.sceneService.destroy(); } + + // 资源管理 } export { Scene }; diff --git a/packages/source/package.json b/packages/source/package.json index df1cf147a5..2ee8927270 100644 --- a/packages/source/package.json +++ b/packages/source/package.json @@ -22,6 +22,7 @@ "license": "ISC", "dependencies": { "@l7/utils": "0.0.1", + "@l7/core": "0.0.1", "@mapbox/geojson-rewind": "^0.4.0", "@turf/helpers": "^6.1.4", "@turf/invariant": "^6.1.2", diff --git a/packages/source/src/factory.ts b/packages/source/src/factory.ts index abf126e7a4..b7c2b5d644 100644 --- a/packages/source/src/factory.ts +++ b/packages/source/src/factory.ts @@ -1,6 +1,7 @@ +import { IParserCfg, ITransform } from '@l7/core'; import { IParserData } from './interface'; type ParserFunction = (data: any, cfg?: any) => IParserData; -type transformFunction = (data: IParserData, cfg?: object) => IParserData; +type transformFunction = (data: IParserData, cfg?: any) => IParserData; const TRANSFORMS: { [type: string]: transformFunction; } = {}; diff --git a/packages/source/src/index.ts b/packages/source/src/index.ts index 7ebdc18515..a307b5857a 100644 --- a/packages/source/src/index.ts +++ b/packages/source/src/index.ts @@ -4,11 +4,15 @@ import geojson from './parser/geojson'; import image from './parser/image'; import json from './parser/json'; import Source from './source'; +import { cluster } from './transform/cluster'; +import { aggregatorToGrid } from './transform/grid'; export default Source; registerParser('geojson', geojson); registerParser('image', image); registerParser('csv', csv); registerParser('json', json); +registerTransform('cluster', cluster); +registerTransform('grid', aggregatorToGrid); export { getTransform, registerTransform, diff --git a/packages/source/src/interface.ts b/packages/source/src/interface.ts index ff9f989e1b..fa6612f2ef 100644 --- a/packages/source/src/interface.ts +++ b/packages/source/src/interface.ts @@ -1,24 +1,4 @@ export type DataType = string | object[] | object; -export interface IParserCfg { - type: string; - x?: string; - y?: string; - x1?: string; - y1?: string; - coordinates?: string; - [key: string]: any; -} -type CallBack = (...args: any[]) => any; -export interface ITransform { - type: string; - [key: string]: any; - callback: CallBack; -} - -export interface ISourceCFG { - parser?: IParserCfg; - transforms?: ITransform[]; -} export interface IDictionary { [key: string]: TValue; } diff --git a/packages/source/src/parser/csv.ts b/packages/source/src/parser/csv.ts index 3e8ccfce1e..7c42bb57ed 100644 --- a/packages/source/src/parser/csv.ts +++ b/packages/source/src/parser/csv.ts @@ -1,5 +1,5 @@ import { csvParse } from 'd3-dsv'; -import { IJsonData, IParserCfg, IParserData } from '../interface'; +import { IJsonData, IParserCfg, IParserData } from '@l7/core'; import json from './json'; export default function csv(data: string, cfg: IParserCfg): IParserData { const csvData: IJsonData = csvParse(data); diff --git a/packages/source/src/parser/image.ts b/packages/source/src/parser/image.ts index 90a5d5dd9c..bd5e912c72 100644 --- a/packages/source/src/parser/image.ts +++ b/packages/source/src/parser/image.ts @@ -1,13 +1,20 @@ +import { getImage } from '@l7/utils'; import { IParserData } from '../interface'; - interface IImageCfg { extent: [number, number, number, number]; } -export default function image(data: string | [], cfg: IImageCfg): IParserData { +export default function image( + data: string | string[], + cfg: IImageCfg, +): IParserData { const { extent } = cfg; - + const images = new Promise((resolve) => { + loadData(data, (res: any) => { + resolve(res); + }); + }); const resultData: IParserData = { - images: loadData(data), + images, _id: 1, dataArray: [ { @@ -18,16 +25,26 @@ export default function image(data: string | [], cfg: IImageCfg): IParserData { }; return resultData; } -function loadData(data: string | string[]): Promise { +function loadData(data: string | string[], done: any) { const url = data; + const imageDatas: HTMLImageElement[] = []; if (typeof url === 'string') { - const imageRequest = new Request(url); - return fetch(imageRequest); - } else { - const fetchs = url.map((item: string) => { - const imageRequest = new Request(item); - return fetch(imageRequest); + getImage({ url }, (err: string, img: HTMLImageElement) => { + imageDatas.push(img); + done(imageDatas); + }); + } else { + const imageCount = url.length; + let imageindex = 0; + url.forEach((item) => { + getImage({ url: item }, (err: any, img: HTMLImageElement) => { + imageindex++; + imageDatas.push(img); + if (imageindex === imageCount) { + done(imageDatas); + } + }); }); - return Promise.all(fetchs); } + return image; } diff --git a/packages/source/src/parser/json.ts b/packages/source/src/parser/json.ts index ca822dc2ad..c42dc57965 100644 --- a/packages/source/src/parser/json.ts +++ b/packages/source/src/parser/json.ts @@ -6,7 +6,7 @@ import { IParseDataItem, IParserCfg, IParserData, -} from '../interface'; +} from '@l7/core'; export default function json(data: IJsonData, cfg: IParserCfg): IParserData { const { x, y, x1, y1, coordinates } = cfg; const resultData: IParseDataItem[] = []; diff --git a/packages/source/src/source.ts b/packages/source/src/source.ts index 1cb2d9c265..48ac92e7fb 100644 --- a/packages/source/src/source.ts +++ b/packages/source/src/source.ts @@ -1,41 +1,67 @@ +import { IParserCfg, IParserData, ISourceCFG, ITransform } from '@l7/core'; import { extent } from '@l7/utils'; import { BBox, FeatureCollection, Geometries, Properties } from '@turf/helpers'; import { EventEmitter } from 'eventemitter3'; import { cloneDeep } from 'lodash'; -import { getParser } from './'; -import { IDictionary, IParserData, ISourceCFG } from './interface'; +import { SyncHook } from 'tapable'; +import { getParser, getTransform } from './'; export default class Source extends EventEmitter { public data: IParserData; // 数据范围 public extent: BBox; - private attrs: IDictionary = {}; + // 生命周期钩子 + public hooks = { + init: new SyncHook(['source']), + layout: new SyncHook(['source']), + update: new SyncHook(['source']), + }; + public parser: IParserCfg = { type: 'geojson' }; + public transforms: ITransform[] = []; // 原始数据 private originData: any; constructor(data: any, cfg?: ISourceCFG) { super(); - this.set('data', data); - Object.assign(this.attrs, cfg); - this.originData = cloneDeep(this.get('data')); + this.data = cloneDeep(data); + this.originData = data; + if (cfg) { + if (cfg.parser) { + this.parser = cfg.parser; + } + if (cfg.transforms) { + this.transforms = cfg.transforms; + } + } + this.hooks.init.tap('parser', () => { + this.excuteParser(); + }); + this.hooks.init.tap('transform', () => { + this.executeTrans(); + }); this.init(); } - public get(name: string): any { - return this.attrs[name]; - } - public set(name: string, value: any) { - this.attrs[name] = value; - } private excuteParser(): void { - const parser = this.get('parser') || {}; + const parser = this.parser; const type: string = parser.type || 'geojson'; const sourceParser = getParser(type); this.data = sourceParser(this.originData, parser); // 计算范围 this.extent = extent(this.data.dataArray); } + /** + * 数据统计 + */ + private executeTrans() { + const trans = this.transforms; + trans.forEach((tran: ITransform) => { + const { type } = tran; + const data = getTransform(type)(this.data, tran); + Object.assign(this.data, data); + }); + } private init() { - this.excuteParser(); // 数据解析 + this.hooks.init.call(this); } } diff --git a/packages/source/src/transform/cluster.ts b/packages/source/src/transform/cluster.ts new file mode 100644 index 0000000000..5ee549286c --- /dev/null +++ b/packages/source/src/transform/cluster.ts @@ -0,0 +1,59 @@ +import { IParserCfg, IParserData, ISourceCFG, ITransform } from '@l7/core'; +import Supercluster from 'supercluster'; +export function cluster(data: IParserData, option: ITransform): IParserData { + const { radius = 80, maxZoom = 18, minZoom = 0, field, zoom = 2 } = option; + if (data.pointIndex) { + const clusterData = data.pointIndex.getClusters(data.extent, zoom); + data.dataArray = formatData(clusterData); + return data; + } + const pointIndex = new Supercluster({ + radius, + minZoom, + maxZoom, + map: (props) => ({ sum: props[field] }), // 根据指定字段求和 + reduce: (accumulated, props) => { + accumulated.sum += props.sum; + }, + }); + const geojson: { + type: string; + features: any[]; + } = { + type: 'FeatureCollection', + features: [], + }; + geojson.features = data.dataArray.map((item) => { + return { + type: 'Feature', + properties: { + [field]: item[field], + }, + geometry: { + type: 'Point', + coordinates: item.coordinates, + }, + }; + }); + pointIndex.load(geojson.features); + const clusterPoint = pointIndex.getClusters(data.extent, zoom); + const resultData = clusterPoint.map((point, index) => { + return { + coordinates: point.geometry.coordinates, + _id: index + 1, + ...point.properties, + }; + }); + data.dataArray = resultData; + data.pointIndex = pointIndex; + return data; +} +export function formatData(clusterPoint: any[]) { + return clusterPoint.map((point, index) => { + return { + coordinates: point.geometry.coordinates, + _id: index + 1, + ...point.properties, + }; + }); +} diff --git a/packages/source/src/transform/grid.ts b/packages/source/src/transform/grid.ts new file mode 100644 index 0000000000..b55581f347 --- /dev/null +++ b/packages/source/src/transform/grid.ts @@ -0,0 +1,116 @@ +/** + * 生成四边形热力图 + */ +import { IParserCfg, IParserData, ISourceCFG, ITransform } from '@l7/core'; +import { max, mean, min, sum } from './statistics'; +const statMap: { [key: string]: any } = { + min, + max, + mean, + sum, +}; +interface IGridHash { + [key: string]: any; +} +interface IGridOffset { + yOffset: number; + xOffset: number; +} +const R_EARTH = 6378000; + +export function aggregatorToGrid(data: IParserData, option: ITransform) { + const dataArray = data.dataArray; + const { size = 10 } = option; + const { gridHash, gridOffset } = _pointsGridHash(dataArray, size); + const layerData = _getGridLayerDataFromGridHash(gridHash, gridOffset, option); + return { + yOffset: gridOffset.yOffset / 1.8, + xOffset: gridOffset.xOffset / 1.8, + radius: gridOffset.xOffset, + dataArray: layerData, + }; +} + +function _pointsGridHash(dataArray: any[], size: number) { + let latMin = Infinity; + let latMax = -Infinity; + let pLat; + for (const point of dataArray) { + pLat = point.coordinates[1]; + if (Number.isFinite(pLat)) { + latMin = pLat < latMin ? pLat : latMin; + latMax = pLat > latMax ? pLat : latMax; + } + } + const centerLat = (latMin + latMax) / 2; + // const centerLat = 34.54083; + const gridOffset = _calculateGridLatLonOffset(size, centerLat); + if (gridOffset.xOffset <= 0 || gridOffset.yOffset <= 0) { + return { gridHash: {}, gridOffset }; + } + const gridHash: IGridHash = {}; + for (const point of dataArray) { + const lat = point.coordinates[1]; + const lng = point.coordinates[0]; + + if (Number.isFinite(lat) && Number.isFinite(lng)) { + const latIdx = Math.floor((lat + 90) / gridOffset.yOffset); + const lonIdx = Math.floor((lng + 180) / gridOffset.xOffset); + const key = `${latIdx}-${lonIdx}`; + + gridHash[key] = gridHash[key] || { count: 0, points: [] }; + gridHash[key].count += 1; + gridHash[key].points.push(point); + } + } + + return { gridHash, gridOffset }; +} +// 计算网格偏移量 +function _calculateGridLatLonOffset(cellSize: number, latitude: number) { + const yOffset = _calculateLatOffset(cellSize); + const xOffset = _calculateLonOffset(latitude, cellSize); + return { yOffset, xOffset }; +} + +function _calculateLatOffset(dy: number) { + return (dy / R_EARTH) * (180 / Math.PI); +} + +function _calculateLonOffset(lat: number, dx: number) { + return ((dx / R_EARTH) * (180 / Math.PI)) / Math.cos((lat * Math.PI) / 180); +} +function _getGridLayerDataFromGridHash( + gridHash: IGridHash, + gridOffset: IGridOffset, + option: ITransform, +) { + return Object.keys(gridHash).reduce((accu, key, i) => { + const idxs = key.split('-'); + const latIdx = parseInt(idxs[0], 10); + const lonIdx = parseInt(idxs[1], 10); + const item: { + [key: string]: any; + } = {}; + if (option.field && option.method) { + const columns = getColumn(gridHash[key].points, option.field); + item[option.method] = statMap[option.method](columns); + } + Object.assign(item, { + _id: i + 1, + coordinates: [ + -180 + gridOffset.xOffset * lonIdx, + -90 + gridOffset.yOffset * latIdx, + ], + count: gridHash[key].count, + }); + // @ts-ignore + accu.push(item); + return accu; + }, []); +} +function getColumn(data: any[], columnName: string) { + return data.map((item) => { + return item[columnName]; + }); +} diff --git a/packages/source/src/transform/statistics.ts b/packages/source/src/transform/statistics.ts new file mode 100644 index 0000000000..e3da89891c --- /dev/null +++ b/packages/source/src/transform/statistics.ts @@ -0,0 +1,71 @@ +function max(x: number[]) { + if (x.length === 0) { + throw new Error('max requires at least one data point'); + } + + let value = x[0]; + for (let i = 1; i < x.length; i++) { + // On the first iteration of this loop, max is + // undefined and is thus made the maximum element in the array + if (x[i] > value) { + value = x[i]; + } + } + return value; +} + +function min(x: number[]) { + if (x.length === 0) { + throw new Error('min requires at least one data point'); + } + + let value = x[0]; + for (let i = 1; i < x.length; i++) { + // On the first iteration of this loop, min is + // undefined and is thus made the minimum element in the array + if (x[i] < value) { + value = x[i]; + } + } + return value; +} + +function sum(x: number[]) { + // If the array is empty, we needn't bother computing its sum + if (x.length === 0) { + return 0; + } + + // Initializing the sum as the first number in the array + let sumNum = x[0]; + + // Keeping track of the floating-point error correction + let correction = 0; + + let transition; + + for (let i = 1; i < x.length; i++) { + transition = sumNum + x[i]; + + // Here we need to update the correction in a different fashion + // if the new absolute value is greater than the absolute sum + if (Math.abs(sumNum) >= Math.abs(x[i])) { + correction += sumNum - transition + x[i]; + } else { + correction += x[i] - transition + sumNum; + } + + sumNum = transition; + } + + // Returning the corrected sum + return sumNum + correction; +} +function mean(x: number[]) { + if (x.length === 0) { + throw new Error('mean requires at least one data point'); + } + return sum(x) / x.length; +} + +export { sum, max, min, mean }; diff --git a/packages/utils/src/fetchData.ts b/packages/utils/src/fetchData.ts new file mode 100644 index 0000000000..a4e874fee6 --- /dev/null +++ b/packages/utils/src/fetchData.ts @@ -0,0 +1,125 @@ +class AJAXError extends Error { + private status: number; + private url: string; + + constructor(message: string, status: number, url: string) { + super(message); + this.status = status; + this.url = url; + + // work around for https://github.com/Rich-Harris/buble/issues/40 + this.name = this.constructor.name; + this.message = message; + } + + public toString() { + return `${this.name}: ${this.message} (${this.status}): ${this.url}`; + } +} + +function makeRequest(requestParameters: any) { + const xhr = new XMLHttpRequest(); + + xhr.open('GET', requestParameters.url, true); + for (const k in requestParameters.headers) { + if (requestParameters.headers.hasOwnProperty(k)) { + xhr.setRequestHeader(k, requestParameters.headers[k]); + } + } + xhr.withCredentials = requestParameters.credentials === 'include'; + return xhr; +} + +export const getJSON = (requestParameters: any, callback: any) => { + const xhr = makeRequest(requestParameters); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.onerror = () => { + callback(new Error(xhr.statusText)); + }; + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300 && xhr.response) { + let data; + try { + data = JSON.parse(xhr.response); + } catch (err) { + return callback(err); + } + callback(null, data); + } else { + if (xhr.status === 401) { + callback( + new AJAXError(`${xhr.statusText}`, xhr.status, requestParameters.url), + ); + } else { + callback( + new AJAXError(xhr.statusText, xhr.status, requestParameters.url), + ); + } + } + }; + xhr.send(); + return xhr; +}; + +export const getArrayBuffer = (requestParameters: any, callback: any) => { + const xhr = makeRequest(requestParameters); + xhr.responseType = 'arraybuffer'; + xhr.onerror = () => { + callback(new Error(xhr.statusText)); + }; + xhr.onload = () => { + const response = xhr.response; + if (response.byteLength === 0 && xhr.status === 200) { + return callback(new Error('http status 200 returned without content.')); + } + if (xhr.status >= 200 && xhr.status < 300 && xhr.response) { + callback(null, { + data: response, + cacheControl: xhr.getResponseHeader('Cache-Control'), + expires: xhr.getResponseHeader('Expires'), + }); + } else { + callback( + new AJAXError(xhr.statusText, xhr.status, requestParameters.url), + ); + } + }; + xhr.send(); + return xhr; +}; + +function sameOrigin(url: string) { + const a = window.document.createElement('a'); + a.href = url; + return ( + a.protocol === window.document.location.protocol && + a.host === window.document.location.host + ); +} + +const transparentPngUrl = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; + +export const getImage = (requestParameters: any, callback: any) => { + // request the image with XHR to work around caching issues + // see https://github.com/mapbox/mapbox-gl-js/issues/1470 + return getArrayBuffer(requestParameters, (err: string, imgData: any) => { + if (err) { + callback(err); + } else if (imgData) { + const img = new window.Image(); + img.crossOrigin = 'anonymous'; + const URL = window.URL || window.webkitURL; + img.onload = () => { + callback(null, img); + URL.revokeObjectURL(img.src); + }; + const blob = new window.Blob([new Uint8Array(imgData.data)], { + type: 'image/png', + }); + img.src = imgData.data.byteLength + ? URL.createObjectURL(blob) + : transparentPngUrl; + } + }); +}; diff --git a/packages/utils/src/geo.ts b/packages/utils/src/geo.ts index c09e37e614..851cc4b62a 100644 --- a/packages/utils/src/geo.ts +++ b/packages/utils/src/geo.ts @@ -1,5 +1,6 @@ import { BBox } from '@turf/helpers'; - +const originShift = (2 * Math.PI * 6378137) / 2.0; +export type Point = [number, number] | [number, number, number]; /** * 计算地理数据范围 * @param {dataArray} data 地理坐标数据 @@ -46,3 +47,110 @@ function transform(item: any[], cb: (item: any[]) => any): any { } return cb(item); } +export function lngLatToMeters(lnglat: Point): Point; +export function lngLatToMeters( + lnglat: Point, + validate: boolean = true, + accuracy = { enable: true, decimal: 1 }, +) { + lnglat = validateLngLat(lnglat, validate); + const lng = lnglat[0]; + const lat = lnglat[1]; + let x = (lng * originShift) / 180.0; + let y = + Math.log(Math.tan(((90 + lat) * Math.PI) / 360.0)) / (Math.PI / 180.0); + y = (y * originShift) / 180.0; + if (accuracy.enable) { + x = Number(x.toFixed(accuracy.decimal)); + y = Number(y.toFixed(accuracy.decimal)); + } + return lnglat.length === 3 ? [x, y, lnglat[2]] : [x, y]; +} + +export function metersToLngLat(meters: Point, decimal = 6) { + const x = meters[0]; + const y = meters[1]; + let lng = (x / originShift) * 180.0; + let lat = (y / originShift) * 180.0; + lat = + (180 / Math.PI) * + (2 * Math.atan(Math.exp((lat * Math.PI) / 180.0)) - Math.PI / 2.0); + if (decimal !== undefined && decimal !== null) { + lng = Number(lng.toFixed(decimal)); + lat = Number(lat.toFixed(decimal)); + } + return meters.length === 3 ? [lng, lat, meters[2]] : [lng, lat]; +} +export function longitude(lng: number) { + if (lng === undefined || lng === null) { + throw new Error('lng is required'); + } + + // lngitudes cannot extends beyond +/-90 degrees + if (lng > 180 || lng < -180) { + lng = lng % 360; + if (lng > 180) { + lng = -360 + lng; + } + if (lng < -180) { + lng = 360 + lng; + } + if (lng === 0) { + lng = 0; + } + } + return lng; +} +export function latitude(lat: number) { + if (lat === undefined || lat === null) { + throw new Error('lat is required'); + } + + if (lat > 90 || lat < -90) { + lat = lat % 180; + if (lat > 90) { + lat = -180 + lat; + } + if (lat < -90) { + lat = 180 + lat; + } + if (lat === 0) { + lat = 0; + } + } + return lat; +} +export function validateLngLat(lnglat: Point, validate: boolean): Point { + if (validate === false) { + return lnglat; + } + + const lng = longitude(lnglat[0]); + let lat = latitude(lnglat[1]); + + // Global Mercator does not support latitudes within 85 to 90 degrees + if (lat > 85) { + lat = 85; + } + if (lat < -85) { + lat = -85; + } + return lnglat.length === 3 ? [lng, lat, lnglat[2]] : [lng, lat]; +} +export function aProjectFlat(lnglat: number[]) { + const maxs = 85.0511287798; + const lat = Math.max(Math.min(maxs, lnglat[1]), -maxs); + const scale = 256 << 20; + let d = Math.PI / 180; + let x = lnglat[0] * d; + let y = lat * d; + y = Math.log(Math.tan(Math.PI / 4 + y / 2)); + + const a = 0.5 / Math.PI; + const b = 0.5; + const c = -0.5 / Math.PI; + d = 0.5; + x = scale * (a * x + b) - 215440491; + y = scale * (c * y + d) - 106744817; + return [parseInt(x.toString(), 10), parseInt(y.toString(), 10)]; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index cd9bb4ac59..e581a9d4e1 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,2 +1,3 @@ export { djb2hash, BKDRHash } from './hash'; -export { extent, tranfrormCoord } from './geo'; +export * from './fetchData'; +export * from './geo'; diff --git a/stories/MapAdaptor/Map.stories.tsx b/stories/MapAdaptor/Map.stories.tsx index a1f0adc11a..677f177358 100644 --- a/stories/MapAdaptor/Map.stories.tsx +++ b/stories/MapAdaptor/Map.stories.tsx @@ -3,6 +3,11 @@ import * as React from 'react'; import AMap from './components/AMap'; import Mapbox from './components/Mapbox'; import Polygon from './components/Polygon'; +import Point3D from './components/Point3D'; +import Line from './components/Line'; +import ImageLayer from './components/Image'; +import GridHeatMap from './components/GridHeatmap'; +import PointImage from './components/pointImage'; // @ts-ignore import notes from './Map.md'; @@ -13,4 +18,9 @@ storiesOf('地图底图测试', module) .add('Mapbox', () => , { notes: { markdown: notes }, }) - .add('Polygon', () => ); + .add('Polygon', () => ) + .add('Point3D', () => ) + .add('Line', () => ) + .add('GridHeatMap', () => ) + .add('Image', () => ) + .add('pointImage', () => ); diff --git a/stories/MapAdaptor/components/GridHeatmap.tsx b/stories/MapAdaptor/components/GridHeatmap.tsx new file mode 100644 index 0000000000..9326fa5ad9 --- /dev/null +++ b/stories/MapAdaptor/components/GridHeatmap.tsx @@ -0,0 +1,77 @@ +import { HeatMapLayer } from '@l7/layers'; +import { Scene } from '@l7/scene'; +import * as React from 'react'; + +export default class GridHeatMap 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/c3f8bda2-081b-449d-aa9f-9413b779205b.json', + ); + const scene = new Scene({ + center: [116.49434030056, 39.868073421167621], + id: 'map', + pitch: 0, + type: 'amap', + style: 'mapbox://styles/mapbox/streets-v9', + zoom: 16, + }); + const layer = new HeatMapLayer({}); + layer + .source(await response.json(), { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + transforms: [ + { + type: 'grid', + size: 50, + field: 'count', + method: 'sum', + }, + ], + }) + .size('sum', (value: number) => { + return value; + }) + .shape('circle') + .style({ + coverage: 1.2, + angle: 0, + }) + .color('count', [ + '#002466', + '#105CB3', + '#2894E0', + '#CFF6FF', + '#FFF5B8', + '#FFAB5C', + '#F27049', + '#730D1C', + ]); + scene.addLayer(layer); + scene.render(); + } + + public render() { + return ( +
+ ); + } +} diff --git a/stories/MapAdaptor/components/Image.tsx b/stories/MapAdaptor/components/Image.tsx new file mode 100644 index 0000000000..abe2bdb95c --- /dev/null +++ b/stories/MapAdaptor/components/Image.tsx @@ -0,0 +1,50 @@ +import { ImageLayer } from '@l7/layers'; +import { Scene } from '@l7/scene'; +import * as React from 'react'; + +export default class ImageLayerDemo extends React.Component { + private scene: Scene; + + public componentWillUnmount() { + this.scene.destroy(); + } + + public componentDidMount() { + const scene = new Scene({ + center: [121.2680, 30.3628], + id: 'map', + pitch: 0, + type: 'mapbox', + style: 'mapbox://styles/mapbox/streets-v9', + zoom: 10, + }); + const layer = new ImageLayer({}); + layer.source( + 'https://gw.alipayobjects.com/zos/rmsportal/FnHFeFklTzKDdUESRNDv.jpg', + { + parser: { + type: 'image', + extent: [121.168, 30.2828, 121.384, 30.4219], + }, + }, + ); + // scene.addLayer(layer); + scene.render(); + console.log(scene); + } + + public render() { + return ( +
+ ); + } +} diff --git a/stories/MapAdaptor/components/Line.tsx b/stories/MapAdaptor/components/Line.tsx new file mode 100644 index 0000000000..590a119418 --- /dev/null +++ b/stories/MapAdaptor/components/Line.tsx @@ -0,0 +1,87 @@ +import { Line } from '@l7/layers'; +import { Scene } from '@l7/scene'; +import * as React from 'react'; + +export default class Point3D extends React.Component { + private scene: Scene; + + public componentWillUnmount() { + this.scene.destroy(); + } + + public async componentDidMount() { + const response = await fetch( + 'https://gw.alipayobjects.com/os/rmsportal/ZVfOvhVCzwBkISNsuKCc.json', + ); + const testdata = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [ + [91.58203125, 34.95799531086792], + [96.767578125, 34.379712580462204], + [99.228515625, 33.7243396617476], + ], + }, + }, + ], + }; + const scene = new Scene({ + center: [102.602992, 23.107329], + id: 'map', + pitch: 0, + type: 'mapbox', + style: 'mapbox://styles/mapbox/dark-v9', + zoom: 13, + }); + const LineLayer = new Line({}); + + LineLayer.source(await response.json()) + .size(1) + .shape('line') + .color( + 'ELEV', + [ + '#E8FCFF', + '#CFF6FF', + '#A1E9ff', + '#65CEF7', + '#3CB1F0', + '#2894E0', + '#1772c2', + '#105CB3', + '#0D408C', + '#002466', + ].reverse(), + ) + .render(); + scene.addLayer(LineLayer); + // function run() { + // scene.render(); + // requestAnimationFrame(run); + // } + // requestAnimationFrame(run); + scene.render(); + this.scene = scene; + console.log(LineLayer); + } + + public render() { + return ( +
+ ); + } +} diff --git a/stories/MapAdaptor/components/Point3D.tsx b/stories/MapAdaptor/components/Point3D.tsx new file mode 100644 index 0000000000..bbe2a407e7 --- /dev/null +++ b/stories/MapAdaptor/components/Point3D.tsx @@ -0,0 +1,64 @@ +import { Point } from '@l7/layers'; +import { Scene } from '@l7/scene'; +import * as React from 'react'; +import data from './data.json'; + +export default class Point3D extends React.Component { + private scene: Scene; + + public componentWillUnmount() { + this.scene.destroy(); + } + + public componentDidMount() { + const scene = new Scene({ + center: [120.19382669582967, 30.258134], + id: 'map', + pitch: 0, + type: 'mapbox', + style: 'mapbox://styles/mapbox/streets-v9', + zoom: 1, + }); + scene.addImage( + '00', + 'https://gw.alipayobjects.com/mdn/antv_site/afts/img/A*kzTMQqS2QdUAAAAAAAAAAABkARQnAQ', + ); + const pointLayer = new Point({}); + const p1 = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: [83.671875, 44.84029065139799], + }, + }, + ], + }; + pointLayer + .source(data) + .color('blue') + .shape('scalerank', [ 'triangleColumn', 'squareColumn', 'hexagonColumn' ,'cylinder' ]) + .size([25, 10]); + scene.addLayer(pointLayer); + scene.render(); + + } + + public render() { + return ( +
+ ); + } +} diff --git a/stories/MapAdaptor/components/Polygon.tsx b/stories/MapAdaptor/components/Polygon.tsx index e8b4665c56..d77e2ee585 100644 --- a/stories/MapAdaptor/components/Polygon.tsx +++ b/stories/MapAdaptor/components/Polygon.tsx @@ -27,7 +27,9 @@ export default class Mapbox extends React.Component { features: [ { type: 'Feature', - properties: {}, + properties: { + name: 'test', + }, geometry: { type: 'Polygon', coordinates: [ @@ -53,21 +55,13 @@ export default class Mapbox extends React.Component { }); const layer = new PolygonLayer({ enableMultiPassRenderer: true, - passes: [ - 'blurH', - [ - 'blurV', - { - blurRadius: 8, - }, - ], - ], - enablePicking: true, + passes: [], }); // TODO: new GeoJSONSource() layer .source(await response.json()) + .size('name', [0, 10000, 50000, 30000, 100000]) .color('name', [ '#2E8AE6', '#69D1AB', @@ -75,12 +69,19 @@ export default class Mapbox extends React.Component { '#FFD591', '#FF7A45', '#CF1D49', - ]); + ]) + .shape('fill') + .style({ + opacity: 0.8, + }); scene.addLayer(layer); - scene.render(); - + function run() { + scene.render(); + requestAnimationFrame(run); + } + requestAnimationFrame(run); this.scene = scene; - + console.log(layer); /*** 运行时修改样式属性 ***/ // const gui = new dat.GUI(); // this.gui = gui; diff --git a/stories/MapAdaptor/components/pointImage.tsx b/stories/MapAdaptor/components/pointImage.tsx new file mode 100644 index 0000000000..2e63242142 --- /dev/null +++ b/stories/MapAdaptor/components/pointImage.tsx @@ -0,0 +1,62 @@ +import { Point } from '@l7/layers'; +import { Scene } from '@l7/scene'; +import * as React from 'react'; +import data from './data.json'; +export default class PointImage extends React.Component { + private scene: Scene; + + public componentWillUnmount() { + this.scene.destroy(); + } + + public componentDidMount() { + const scene = new Scene({ + center: [120.19382669582967, 30.258134], + id: 'map', + pitch: 0, + type: 'mapbox', + style: 'mapbox://styles/mapbox/streets-v9', + zoom: 1, + }); + scene.addImage( + '00', + 'https://gw.alipayobjects.com/mdn/antv_site/afts/img/A*kzTMQqS2QdUAAAAAAAAAAABkARQnAQ', + ); + const pointLayer = new Point({}); + const p1 = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: [83.671875, 44.84029065139799], + }, + }, + ], + }; + pointLayer + .source(data) + // .color('blue') + .shape('00') + .size(14); + scene.addLayer(pointLayer); + scene.render(); + } + + public render() { + return ( +
+ ); + } +} diff --git a/yarn.lock b/yarn.lock index 0522495639..dbc4a9652f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2754,6 +2754,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.0.0.tgz#a0d63a296a2d8435a9ec59393dcac746c6174a96" integrity sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA== +"@types/d3-color@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.2.tgz#80cf7cfff7401587b8f89307ba36fe4a576bc7cf" + integrity sha512-6pBxzJ8ZP3dYEQ4YjQ+NVbQaOflfgXq/JbDiS99oLobM2o72uAST4q6yPxHv6FOTCRC/n35ktuo8pvw/S4M7sw== + "@types/d3-dsv@^1.0.36": version "1.0.36" resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.36.tgz#e91129d7c02b1b814838d001e921e8b9a67153d0" @@ -5562,6 +5567,11 @@ d3-color@1: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.3.0.tgz#675818359074215b020dc1d41d518136dcb18fa9" integrity sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg== +d3-color@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.0.tgz#89c45a995ed773b13314f06460df26d60ba0ecaf" + integrity sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg== + d3-dsv@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.1.1.tgz#aaa830ecb76c4b5015572c647cc6441e3c7bb701" @@ -7285,6 +7295,11 @@ gl-matrix@^3.0.0, gl-matrix@^3.1.0: resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.1.0.tgz#f5b2de17d8fed95a79e5025b10cded0ab9ccbed0" integrity sha512-526NA+3EA+ztAQi0IZpSWiM0fyQXIp7IbRvfJ4wS/TjjQD0uv0fVybXwwqqSOlq33UckivI0yMDlVtboWm3k7A== +gl-vec2@^1.0.0, gl-vec2@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/gl-vec2/-/gl-vec2-1.3.0.tgz#83d472ed46034de8e09cbc857123fb6c81c51199" + integrity sha512-YiqaAuNsheWmUV0Sa8k94kBB0D6RWjwZztyO+trEYS8KzJ6OQB/4686gdrf59wld4hHFIvaxynO3nRxpk1Ij/A== + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -11377,6 +11392,13 @@ polished@^3.3.1: dependencies: "@babel/runtime" "^7.4.5" +polyline-miter-util@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/polyline-miter-util/-/polyline-miter-util-1.0.1.tgz#b693f2389ea0ded36a6bcf5ecd2ece4b6917d957" + integrity sha1-tpPyOJ6g3tNqa89ezS7OS2kX2Vc= + dependencies: + gl-vec2 "^1.0.0" + popper.js@^1.14.4, popper.js@^1.14.7: version "1.15.0" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"