diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ede8adfc89..793aede385 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -35,6 +35,7 @@ export { }; /** 暴露服务接口供其他 packages 实现 */ +export * from './services/layer/ILayerStyleService'; export * from './services/layer/ILayerService'; export * from './services/source/ISourceService'; export * from './services/map/IMapService'; 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/config/ConfigService.ts b/packages/core/src/services/config/ConfigService.ts index 2d6ff3cabf..c36fe0b257 100644 --- a/packages/core/src/services/config/ConfigService.ts +++ b/packages/core/src/services/config/ConfigService.ts @@ -45,4 +45,4 @@ export default class GlobalConfigService implements IGlobalConfigService { public reset() { this.config = defaultGlobalConfig; } -} +} \ No newline at end of file diff --git a/packages/core/src/services/layer/ILayerService.ts b/packages/core/src/services/layer/ILayerService.ts index 49ce82b515..eddba5482b 100644 --- a/packages/core/src/services/layer/ILayerService.ts +++ b/packages/core/src/services/layer/ILayerService.ts @@ -3,7 +3,7 @@ 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,30 @@ export interface IScale { format?: () => any; domain?: any[]; } +export interface IStyleScale { + scale: any; + field: string; + type: StyleScaleType; +} 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 interface ILayerStyleAttribute { - type?: string; - names?: string[]; - field?: StyleAttributeField; + type: string; + names: string[]; + field: StyleAttributeField; values?: any[]; scales?: any[]; + setScales: (scales: IStyleScale[]) => void; callback?: (...args: any[]) => []; mapping?(...params: unknown[]): unknown[]; } @@ -61,15 +71,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: string, value?: StyleAttributeOption): ILayer; + color(field: string, value?: StyleAttributeOption): ILayer; + shape(field: string, 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 +92,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/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/shaders/lighting.glsl b/packages/core/src/shaders/lighting.glsl new file mode 100644 index 0000000000..fd3b5a1007 --- /dev/null +++ b/packages/core/src/shaders/lighting.glsl @@ -0,0 +1,98 @@ +// Blinn-Phong model +// apply lighting in vertex shader instead of fragment shader +// @see https://learnopengl.com/Advanced-Lighting/Advanced-Lighting +// TODO: support point light、spot light & sun light +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/layers/package.json b/packages/layers/package.json index f1390a2970..6ad99af8ac 100644 --- a/packages/layers/package.json +++ b/packages/layers/package.json @@ -26,6 +26,7 @@ "d3-scale": "^3.1.0", "earcut": "^2.2.1", "eventemitter3": "^3.1.0", + "gl-matrix": "^3.1.0", "lodash": "^4.17.15", "tapable": "^2.0.0-beta.8" }, diff --git a/packages/layers/src/core/BaseBuffer.ts b/packages/layers/src/core/BaseBuffer.ts index 5fd12f9d6a..0b542ce15f 100644 --- a/packages/layers/src/core/BaseBuffer.ts +++ b/packages/layers/src/core/BaseBuffer.ts @@ -5,6 +5,8 @@ interface IBufferCfg { } type Position = number[]; type Color = [number, number, number, number]; +import { lngLatToMeters } from '@l7/utils'; +import { vec3 } from 'gl-matrix'; export interface IBufferInfo { vertices?: any; indexArray?: any; @@ -40,6 +42,44 @@ export default class Buffer { this.uv = !!uv; this.init(); } + public computeVertexNormals() { + const normals = (this.attributes.normals = new Float32Array( + this.verticesCount * 3, + )); + const indexArray = this.indexArray; + const { positions } = this.attributes; + 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] = lngLatToMeters([positions[vA], positions[vA + 1]]); + const pA = vec3.fromValues(ax, ay, positions[vA + 2]); + const [bx, by] = lngLatToMeters([positions[vB], positions[vB + 1]]); + const pB = vec3.fromValues(bx, by, positions[vB + 2]); + const [cx, cy] = lngLatToMeters([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 +87,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) { @@ -104,7 +146,7 @@ export default class Buffer { } } protected calculateWall(feature: IEncodeFeature) { - const size = feature.size; + const size = feature.size || 0; const { vertices, indexOffset, @@ -114,8 +156,11 @@ export default class Buffer { } = 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); + const prePoint = vertices.slice(i * dimensions, (i + 1) * dimensions); + const nextPoint = vertices.slice( + (i + 1) * dimensions, + (i + 2) * dimensions, + ); this.calculateExtrudeFace( prePoint, nextPoint, @@ -165,7 +210,9 @@ export default class Buffer { } private init() { + // 将每个多边形三角化,存储顶点坐标和索引坐标 this.calculateFeatures(); + // 拼接成一个 attribute this.initAttributes(); this.buildFeatures(); } @@ -181,4 +228,18 @@ export default class Buffer { } this.indexArray = new Uint32Array(this.indexCount); } + + 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 fbbfcaf7f8..2fe50e92f5 100644 --- a/packages/layers/src/core/BaseLayer.ts +++ b/packages/layers/src/core/BaseLayer.ts @@ -4,17 +4,21 @@ import { ILayerInitializationOptions, ILayerPlugin, ILayerStyleAttribute, + ILayerStyleOptions, IModel, IMultiPassRenderer, IRendererService, + ISource, lazyInject, StyleAttributeField, + StyleAttributeOption, TYPES, } from '@l7/core'; import Source, { ISourceCFG } 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,14 +41,19 @@ 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; // 样式属性 public styleAttributes: { - [key: string]: Partial; + [key: string]: Required; } = {}; protected layerSource: Source; @@ -77,7 +86,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, @@ -86,7 +98,11 @@ 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, @@ -95,7 +111,11 @@ 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, @@ -105,11 +125,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(options: ILayerStyleOptions): ILayer { + this.styleOption = options; // TODO: merge 默认同类型 return this; } - public render(): ILayer { if (this.multiPassRenderer && this.multiPassRenderer.getRenderFlag()) { this.multiPassRenderer.render(); @@ -124,11 +150,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..2a308692d3 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,27 @@ 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, }; 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 +58,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,7 +67,7 @@ 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); } return scale; @@ -78,7 +84,7 @@ export default class ScaleController { } private getScaleCfg(type: ScaleTypes, field: string, data: any[]) { - const cfg: IScale = { + const cfg: IScaleOption = { field, type, }; @@ -92,11 +98,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](); if (scaleOption.hasOwnProperty('domain')) { - scale.domain(scaleOption.domain); + // 处理同一字段映射不同视觉通道的问题 + 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..dbb39d0382 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,17 @@ export default class StyleAttribute implements ILayerStyleAttribute { this.type = type; this.scales = scales; this.values = values; - this.names = names; + this.names = this.parseFields(field) || []; // 设置 range TODO 2维映射 - this.scales.forEach((scale) => { - scale.scale.range(values); - }); + // this.scales.forEach((scale) => { + // scale.scale.range(values); + // if (scale.type === StyleScaleType.VARIABLE) { + // this.type = StyleScaleType.VARIABLE; + // } + // }); + if (callback) { + this.type = StyleScaleType.VARIABLE; + } this.callback = (...params: any[]): any[] => { /** * 当用户设置的 callback 返回 null 时, 应该返回默认 callback 中的值 @@ -44,7 +49,20 @@ 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) => { + 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 +78,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/plugins/DataEncodePlugin.ts b/packages/layers/src/plugins/DataEncodePlugin.ts index 32f59bcbcc..d86f342e35 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) => { + attribute.names.forEach((field: string) => { scales.push(this.getOrCreateScale(attribute, dataArray)); }); - attribute.scales = scales; + attribute.setScales(scales); }); - // mapping with source data layer.setEncodedData(this.mapping(layer.styleAttributes, dataArray)); }); @@ -55,33 +52,24 @@ export default class DataEncodePlugin implements ILayerPlugin { // layer.hooks.beforeRender.tap() } - private getOrCreateScale(attribute: ILayerStyleAttribute, data: any[]) { + private getOrCreateScale( + attribute: ILayerStyleAttribute, + data: any[], + ): IStyleScale { const { field } = attribute; 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) { + 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 +81,14 @@ export default class DataEncodePlugin implements ILayerPlugin { id: record._id, coordinates: record.coordinates, }; - // TODO 数据过滤 + // 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 +108,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, value } = 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..950d7814a8 --- /dev/null +++ b/packages/layers/src/plugins/DataSourcePlugin.ts @@ -0,0 +1,20 @@ +import { + IGlobalConfigService, + ILayer, + ILayerPlugin, + ILayerStyleAttribute, + IParseDataItem, + IStyleScale, + lazyInject, + StyleScaleType, + TYPES, +} from '@l7/core'; +import Source, { ISourceCFG } 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/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/polygon/buffers/ExtrudeBuffer.ts b/packages/layers/src/polygon/buffers/ExtrudeBuffer.ts new file mode 100644 index 0000000000..19ec468fab --- /dev/null +++ b/packages/layers/src/polygon/buffers/ExtrudeBuffer.ts @@ -0,0 +1,67 @@ +import earcut from 'earcut'; +import BufferBase, { IBufferInfo, IEncodeFeature } 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; + 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 || 1; + const { + indexArray, + vertices, + indexOffset, + verticesOffset, + dimensions, + } = feature.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); + } + } + feature.bufferInfo.verticesOffset += pointCount; + // 添加顶点索引 + this.indexArray.set(indexArray, indexOffset); // 顶部坐标 + feature.bufferInfo.indexOffset += indexArray.length; + } +} 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 dd90ab97fb..0b9f93ad62 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'; @@ -19,14 +20,6 @@ export default class PolygonLayer extends BaseLayer { @lazyInject(TYPES.IRendererService) private readonly renderer: IRendererService; - public style(options: any) { - // this.layerStyleService.update(options); - // this.styleOptions = { - // ...this.styleOptions, - // ...options, - // }; - } - protected renderModels() { this.models.forEach((model) => model.draw({ @@ -46,10 +39,15 @@ export default class PolygonLayer extends BaseLayer { this.models = []; const { vs, fs, uniforms } = this.shaderModule.getModule('polygon'); - const buffer = new FillBuffer({ + const buffer = new ExtrudeBuffer({ data: this.getEncodedData(), }); - + buffer.computeVertexNormals(); + const buffer2 = new FillBuffer({ + data: this.getEncodedData(), + }); + console.log(buffer); + console.log(buffer2); const { createAttribute, createBuffer, @@ -67,6 +65,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, @@ -75,7 +80,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/utils/src/geo.ts b/packages/utils/src/geo.ts index c09e37e614..906cd2011c 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; +type Point = [number, number] | [number, number, number]; /** * 计算地理数据范围 * @param {dataArray} data 地理坐标数据 @@ -46,3 +47,93 @@ 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]; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index cd9bb4ac59..7165880398 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,2 +1,2 @@ export { djb2hash, BKDRHash } from './hash'; -export { extent, tranfrormCoord } from './geo'; +export * from './geo'; diff --git a/stories/MapAdaptor/components/Polygon.tsx b/stories/MapAdaptor/components/Polygon.tsx index 19a1a95cda..92d5ba42ac 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,20 +55,13 @@ export default class Mapbox extends React.Component { }); const layer = new PolygonLayer({ enableMultiPassRenderer: true, - passes: [ - 'blurH', - [ - 'blurV', - { - blurRadius: 8, - }, - ], - ], + passes: [], }); // TODO: new GeoJSONSource() layer .source(await response.json()) + .size('name', [0, 10000, 50000, 30000, 100000]) .color('name', [ '#2E8AE6', '#69D1AB', @@ -74,12 +69,15 @@ export default class Mapbox extends React.Component { '#FFD591', '#FF7A45', '#CF1D49', - ]); + ]) + .shape('fill') + .style({ + opacity: 0.8, + }); scene.addLayer(layer); scene.render(); - this.scene = scene; - + console.log(layer); /*** 运行时修改样式属性 ***/ }