diff --git a/docs/api-reference/carto/data-sources.md b/docs/api-reference/carto/data-sources.md index fb2ccf85f82..2751c6f4a15 100644 --- a/docs/api-reference/carto/data-sources.md +++ b/docs/api-reference/carto/data-sources.md @@ -11,8 +11,8 @@ import {vectorTableSource} from '@deck.gl/carto'; const data = vectorTableSource({ accessToken: 'XXX', connectionName: 'carto_dw', - tableName: 'carto-demo-data.demo_tables.chicago_crime_sample', -}) + tableName: 'carto-demo-data.demo_tables.chicago_crime_sample' +}); ``` ### Promise API @@ -51,7 +51,7 @@ type SourceOptions = { apiBaseUrl?: string; clientId?: string; headers?: Record; - mapsUrl?: string; + maxLengthURL?: number; }; ``` @@ -64,7 +64,7 @@ type VectorTableSourceOptions = { columns?: string[]; spatialDataColumn?: string; tableName: string; -} +}; ``` #### vectorQuerySource @@ -74,7 +74,7 @@ type VectorQuerySourceOptions = { spatialDataColumn?: string; sqlQuery: string; queryParameters: QueryParameters; -} +}; ``` #### vectorTilesetSource @@ -82,7 +82,7 @@ type VectorQuerySourceOptions = { ```ts type VectorTilesetSourceOptions = { tableName: string; -} +}; ``` #### h3TableSource @@ -94,7 +94,7 @@ type H3TableSourceOptions = { columns?: string[]; spatialDataColumn?: string; tableName: string; -} +}; ``` #### h3QuerySource @@ -106,7 +106,7 @@ type H3QuerySourceOptions = { spatialDataColumn?: string; sqlQuery: string; queryParameters: QueryParameters; -} +}; ``` #### h3TilesetSource @@ -114,7 +114,7 @@ type H3QuerySourceOptions = { ```ts type H3TilesetSourceOptions = { tableName: string; -} +}; ``` #### quadbinTableSource @@ -126,7 +126,7 @@ type QuadbinTableSourceOptions = { columns?: string[]; spatialDataColumn?: string; tableName: string; -} +}; ``` #### quadbinQuerySource @@ -138,7 +138,7 @@ type QuadbinQuerySourceOptions = { spatialDataColumn?: string; sqlQuery: string; queryParameters: QueryParameters; -} +}; ``` #### quadbinTilesetSource @@ -146,7 +146,7 @@ type QuadbinQuerySourceOptions = { ```ts type QuadbinTilesetSourceOptions = { tableName: string; -} +}; ``` #### rasterTilesetSource (Experimental) @@ -154,7 +154,7 @@ type QuadbinTilesetSourceOptions = { ```ts type RasterTilesetSourceOptions = { tableName: string; -} +}; ``` Boundary sources are experimental sources where both the tileset and the properties props need a specific schema to work. [Read more about Boundaries in the CARTO documentation](https://docs.carto.com/carto-for-developers/guides/use-boundaries-in-your-application). @@ -166,7 +166,7 @@ type BoundaryTableSourceOptions = { tilesetTableName: string; columns?: string[]; propertiesTableName: string; -} +}; ``` #### boundaryQuerySource (Experimental) @@ -176,7 +176,7 @@ type BoundaryQuerySourceOptions = { tilesetTableName: string; propertiesSqlQuery: string; queryParameters?: QueryParameters; -} +}; ``` ### QueryParameters @@ -184,6 +184,7 @@ type BoundaryQuerySourceOptions = { QueryParameters are used to parametrize SQL queries. The format depends on the source's provider, some examples: [PostgreSQL and Redshift](https://node-postgres.com/features/queries): + ```ts vectorQuerySource({ ..., @@ -193,6 +194,7 @@ vectorQuerySource({ ``` [BigQuery positional](https://cloud.google.com/bigquery/docs/parameterized-queries#node.js): + ```ts vectorQuerySource({ ..., @@ -201,8 +203,8 @@ vectorQuerySource({ }) ``` - [BigQuery named parameters](https://cloud.google.com/bigquery/docs/parameterized-queries#node.js): + ```ts vectorQuerySource({ ..., @@ -212,6 +214,7 @@ vectorQuerySource({ ``` [Snowflake positional](https://docs.snowflake.com/en/user-guide/nodejs-driver-use.html#binding-statement-parameters) : + ```ts vectorQuerySource({ ..., @@ -230,6 +233,7 @@ vectorQuerySource({ ``` [Databricks ODBC](https://github.com/markdirish/node-odbc#bindparameters-callback) + ```ts vectorQuerySource({ ... diff --git a/modules/aggregation-layers/src/common/aggregator/gpu-aggregator/aggregation-transform-uniforms.ts b/modules/aggregation-layers/src/common/aggregator/gpu-aggregator/aggregation-transform-uniforms.ts index 2de5e58d779..8a0bccca8d3 100644 --- a/modules/aggregation-layers/src/common/aggregator/gpu-aggregator/aggregation-transform-uniforms.ts +++ b/modules/aggregation-layers/src/common/aggregator/gpu-aggregator/aggregation-transform-uniforms.ts @@ -15,7 +15,6 @@ export type AggregatorTransformProps = { binIdRange: NumberArray4; isCount: NumberArray3; isMean: NumberArray3; - naN: number; bins: Texture; }; @@ -25,7 +24,6 @@ export const aggregatorTransformUniforms = { uniformTypes: { binIdRange: 'vec4', isCount: 'vec3', - isMean: 'vec3', - naN: 'f32' + isMean: 'vec3' } } as const satisfies ShaderModule; diff --git a/modules/aggregation-layers/src/common/aggregator/gpu-aggregator/webgl-aggregation-transform.ts b/modules/aggregation-layers/src/common/aggregator/gpu-aggregator/webgl-aggregation-transform.ts index 60f6e228b04..6773a108e27 100644 --- a/modules/aggregation-layers/src/common/aggregator/gpu-aggregator/webgl-aggregation-transform.ts +++ b/modules/aggregation-layers/src/common/aggregator/gpu-aggregator/webgl-aggregation-transform.ts @@ -35,9 +35,6 @@ export class WebGLAggregationTransform { this.device = device; this.channelCount = props.channelCount; this.transform = createTransform(device, props); - // Passed in as uniform because 1) there is no GLSL symbol for NaN 2) any expression that exploits undefined behavior to produces NaN - // will subject to platform differences and shader optimization - this.transform.model.shaderInputs.setProps({aggregatorTransform: {naN: NaN}}); this.domainFBO = createRenderTarget(device, 2, 1); } @@ -145,6 +142,8 @@ flat out vec2 values; flat out vec3 values; #endif +const float NAN = intBitsToFloat(-1); + void main() { int row = gl_VertexID / SAMPLER_WIDTH; int col = gl_VertexID - row * SAMPLER_WIDTH; @@ -155,7 +154,7 @@ void main() { aggregatorTransform.isMean ); if (weights.a == 0.0) { - value3 = vec3(aggregatorTransform.naN); + value3 = vec3(NAN); } #if NUM_DIMS == 1 @@ -199,11 +198,6 @@ flat in vec3 values; out vec4 fragColor; void main() { -#if NUM_CHANNELS > 1 - if (isnan(values.x)) discard; -#else - if (isnan(values)) discard; -#endif vec3 value3; #if NUM_CHANNELS == 3 value3 = values; @@ -212,6 +206,7 @@ void main() { #else value3.x = values; #endif + if (isnan(value3.x)) discard; // This shader renders into a 2x1 texture with blending=max // The left pixel yields the max value of each channel // The right pixel yields the min value of each channel diff --git a/modules/arcgis/src/commons.ts b/modules/arcgis/src/commons.ts index ecb3c5811fc..7dbc785c3a8 100644 --- a/modules/arcgis/src/commons.ts +++ b/modules/arcgis/src/commons.ts @@ -1,9 +1,10 @@ /* eslint-disable no-invalid-this */ import {GL} from '@luma.gl/constants'; -import {Model, Geometry} from '@luma.gl/engine'; -import {Deck} from '@deck.gl/core'; import type {Device, Texture, Framebuffer} from '@luma.gl/core'; +import {Deck} from '@deck.gl/core'; +import {Model, Geometry} from '@luma.gl/engine'; +import {WebGLDevice} from '@luma.gl/webgl'; interface Renderer { redraw: () => void; @@ -146,33 +147,35 @@ export function render( ) { const {model, deck, fbo} = resources; const device = model.device; - // @ts-ignore device.getParametersWebGL should return `any` not `void`? - const screenFbo: Framebuffer = device.getParametersWebGL(GL.FRAMEBUFFER_BINDING); - const {width, height, ...viewState} = viewport; + if (device instanceof WebGLDevice) { + // @ts-ignore device.getParametersWebGL should return `any` not `void`? + const screenFbo: Framebuffer = device.getParametersWebGL(GL.FRAMEBUFFER_BINDING); + const {width, height, ...viewState} = viewport; - /* global window */ - const dpr = window.devicePixelRatio; - const pixelWidth = Math.round(width * dpr); - const pixelHeight = Math.round(height * dpr); + /* global window */ + const dpr = window.devicePixelRatio; + const pixelWidth = Math.round(width * dpr); + const pixelHeight = Math.round(height * dpr); - fbo.resize({width: pixelWidth, height: pixelHeight}); + fbo.resize({width: pixelWidth, height: pixelHeight}); - deck.setProps({viewState}); - // redraw deck immediately into deckFbo - deck.redraw('arcgis'); + deck.setProps({viewState}); + // redraw deck immediately into deckFbo + deck.redraw('arcgis'); - // We overlay the texture on top of the map using the full-screen quad. + // We overlay the texture on top of the map using the full-screen quad. - const textureToScreenPass = device.beginRenderPass({ - framebuffer: screenFbo, - parameters: {viewport: [0, 0, pixelWidth, pixelHeight]}, - clearColor: false, - clearDepth: false - }); - try { - model.draw(textureToScreenPass); - } finally { - textureToScreenPass.end(); + const textureToScreenPass = device.beginRenderPass({ + framebuffer: screenFbo, + parameters: {viewport: [0, 0, pixelWidth, pixelHeight]}, + clearColor: false, + clearDepth: false + }); + try { + model.draw(textureToScreenPass); + } finally { + textureToScreenPass.end(); + } } } diff --git a/modules/carto/src/api/common.ts b/modules/carto/src/api/common.ts index 147c0183f5f..120ffa321e0 100644 --- a/modules/carto/src/api/common.ts +++ b/modules/carto/src/api/common.ts @@ -1,4 +1,6 @@ export const DEFAULT_API_BASE_URL = 'https://gcp-us-east1.api.carto.com'; export const DEFAULT_CLIENT = 'deck-gl-carto'; export const V3_MINOR_VERSION = '3.4'; -export const MAX_GET_LENGTH = 8192; + +// Fastly default limit is 8192; leave some padding. +export const DEFAULT_MAX_LENGTH_URL = 7000; diff --git a/modules/carto/src/api/fetch-map.ts b/modules/carto/src/api/fetch-map.ts index 757a2532866..a4950674a57 100644 --- a/modules/carto/src/api/fetch-map.ts +++ b/modules/carto/src/api/fetch-map.ts @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import {CartoAPIError} from './carto-api-error'; -import {DEFAULT_API_BASE_URL, DEFAULT_CLIENT} from './common'; +import {DEFAULT_API_BASE_URL, DEFAULT_CLIENT, DEFAULT_MAX_LENGTH_URL} from './common'; import {buildPublicMapUrl, buildStatsUrl} from './endpoints'; import { GeojsonResult, @@ -36,13 +36,14 @@ type Dataset = { }; /* global clearInterval, setInterval, URL */ -/* eslint-disable complexity, max-statements */ +/* eslint-disable complexity, max-statements, max-params */ async function _fetchMapDataset( dataset: Dataset, accessToken: string, apiBaseUrl: string, clientId?: string, - headers?: Record + headers?: Record, + maxLengthURL = DEFAULT_MAX_LENGTH_URL ) { const { aggregationExp, @@ -64,7 +65,8 @@ async function _fetchMapDataset( clientId, connectionName, format, - headers + headers, + maxLengthURL }; if (type === 'tileset') { @@ -114,7 +116,8 @@ async function _fetchTilestats( attribute: string, dataset: Dataset, accessToken: string, - apiBaseUrl: string + apiBaseUrl: string, + maxLengthURL = DEFAULT_MAX_LENGTH_URL ) { const {connectionName, data, id, source, type, queryParameters} = dataset; const errorContext: APIErrorContext = { @@ -144,7 +147,8 @@ async function _fetchTilestats( baseUrl, headers, parameters, - errorContext + errorContext, + maxLengthURL }); // Replace tilestats for attribute with value from API @@ -158,17 +162,19 @@ async function fillInMapDatasets( {datasets, token}: {datasets: Dataset[]; token: string}, clientId: string, apiBaseUrl: string, - headers?: Record + headers?: Record, + maxLengthURL = DEFAULT_MAX_LENGTH_URL ) { const promises = datasets.map(dataset => - _fetchMapDataset(dataset, token, apiBaseUrl, clientId, headers) + _fetchMapDataset(dataset, token, apiBaseUrl, clientId, headers, maxLengthURL) ); return await Promise.all(promises); } async function fillInTileStats( {datasets, keplerMapConfig, token}: {datasets: Dataset[]; keplerMapConfig: any; token: string}, - apiBaseUrl: string + apiBaseUrl: string, + maxLengthURL = DEFAULT_MAX_LENGTH_URL ) { const attributes: {attribute: string; dataset: any}[] = []; const {layers} = keplerMapConfig.config.visState; @@ -197,7 +203,7 @@ async function fillInTileStats( } const promises = filteredAttributes.map(({attribute, dataset}) => - _fetchTilestats(attribute, dataset, token, apiBaseUrl) + _fetchTilestats(attribute, dataset, token, apiBaseUrl, maxLengthURL) ); return await Promise.all(promises); } @@ -237,6 +243,13 @@ export type FetchMapOptions = { * Callback function that will be invoked whenever data in layers is changed. If provided, `autoRefresh` must also be provided. */ onNewData?: (map: any) => void; + + /** + * Maximum URL character length. Above this limit, requests use POST. + * Used to avoid browser and CDN limits. + * @default {@link DEFAULT_MAX_LENGTH_URL} + */ + maxLengthURL?: number; }; export type FetchMapResult = ParseMapResult & { @@ -255,7 +268,8 @@ export async function fetchMap({ clientId = DEFAULT_CLIENT, headers = {}, autoRefresh, - onNewData + onNewData, + maxLengthURL = DEFAULT_MAX_LENGTH_URL }: FetchMapOptions): Promise { assert(cartoMapId, 'Must define CARTO map id: fetchMap({cartoMapId: "XXXX-XXXX-XXXX"})'); assert(apiBaseUrl, 'Must define apiBaseUrl'); @@ -275,7 +289,7 @@ export async function fetchMap({ const baseUrl = buildPublicMapUrl({apiBaseUrl, cartoMapId}); const errorContext: APIErrorContext = {requestType: 'Public map', mapId: cartoMapId}; - const map = await requestWithParameters({baseUrl, headers, errorContext}); + const map = await requestWithParameters({baseUrl, headers, errorContext, maxLengthURL}); // Periodically check if the data has changed. Note that this // will not update when a map is published. @@ -283,10 +297,16 @@ export async function fetchMap({ if (autoRefresh) { // eslint-disable-next-line @typescript-eslint/no-misused-promises const intervalId = setInterval(async () => { - const changed = await fillInMapDatasets(map, clientId, apiBaseUrl, { - ...headers, - 'If-Modified-Since': new Date().toUTCString() - }); + const changed = await fillInMapDatasets( + map, + clientId, + apiBaseUrl, + { + ...headers, + 'If-Modified-Since': new Date().toUTCString() + }, + maxLengthURL + ); if (onNewData && changed.some(v => v === true)) { onNewData(parseMap(map)); } @@ -315,11 +335,11 @@ export async function fetchMap({ fetchBasemapProps({config: map.keplerMapConfig.config, errorContext}), // Mutates map.datasets so that dataset.data contains data - fillInMapDatasets(map, clientId, apiBaseUrl, headers) + fillInMapDatasets(map, clientId, apiBaseUrl, headers, maxLengthURL) ]); // Mutates attributes in visualChannels to contain tile stats - await fillInTileStats(map, apiBaseUrl); + await fillInTileStats(map, apiBaseUrl, maxLengthURL); const out = {...parseMap(map), basemap, ...{stopAutoRefresh}}; diff --git a/modules/carto/src/api/query.ts b/modules/carto/src/api/query.ts index 060c23039ae..db91dc0a431 100644 --- a/modules/carto/src/api/query.ts +++ b/modules/carto/src/api/query.ts @@ -11,6 +11,7 @@ export const query = async function (options: QueryOptions): Promise({ baseUrl, parameters = {}, headers: customHeaders = {}, - errorContext + errorContext, + maxLengthURL = DEFAULT_MAX_LENGTH_URL }: { baseUrl: string; parameters?: Record; headers?: Record; errorContext: APIErrorContext; + maxLengthURL?: number; }): Promise { parameters = {...DEFAULT_PARAMETERS, ...parameters}; baseUrl = excludeURLParameters(baseUrl, Object.keys(parameters)); @@ -44,7 +46,7 @@ export async function requestWithParameters({ /* global fetch */ const fetchPromise = - url.length > MAX_GET_LENGTH + url.length > maxLengthURL ? fetch(baseUrl, {method: 'POST', body: JSON.stringify(parameters), headers}) : fetch(url, {headers}); diff --git a/modules/carto/src/layers/cluster-tile-layer.ts b/modules/carto/src/layers/cluster-tile-layer.ts index a0be77da4f3..2c99363436c 100644 --- a/modules/carto/src/layers/cluster-tile-layer.ts +++ b/modules/carto/src/layers/cluster-tile-layer.ts @@ -106,8 +106,14 @@ class ClusterGeoJsonLayer< clusterIds: bigint[]; hoveredFeatureId: bigint | number | null; highlightColor: number[]; + aggregationCache: WeakMap[]>>; }; + initializeState() { + super.initializeState(); + this.state.aggregationCache = new WeakMap(); + } + renderLayers(): Layer | null | LayersList { const visibleTiles = this.state.tileset?.tiles.filter((tile: Tile2DHeader) => { return tile.isLoaded && tile.content && this.state.tileset!.isTileVisible(tile); @@ -119,6 +125,7 @@ class ClusterGeoJsonLayer< const {zoom} = this.context.viewport; const {clusterLevel, getPosition, getWeight} = this.props; + const {aggregationCache} = this.state; const properties = extractAggregationProperties(visibleTiles[0]); const data = [] as ClusteredFeaturePropertiesT[]; @@ -127,15 +134,21 @@ class ClusterGeoJsonLayer< // Calculate aggregation based on viewport zoom const overZoom = Math.round(zoom - tile.zoom); const aggregationLevels = Math.round(clusterLevel) - overZoom; + let tileAggregationCache = aggregationCache.get(tile.content); + if (!tileAggregationCache) { + tileAggregationCache = new Map(); + aggregationCache.set(tile.content, tileAggregationCache); + } const didAggregate = aggregateTile( tile, + tileAggregationCache, aggregationLevels, properties, getPosition, getWeight ); needsUpdate ||= didAggregate; - data.push(...tile.userData![aggregationLevels]); + data.push(...tileAggregationCache.get(aggregationLevels)!); } data.sort((a, b) => Number(b.count - a.count)); diff --git a/modules/carto/src/layers/cluster-utils.ts b/modules/carto/src/layers/cluster-utils.ts index 494a99901e3..e8fa54217c7 100644 --- a/modules/carto/src/layers/cluster-utils.ts +++ b/modules/carto/src/layers/cluster-utils.ts @@ -23,6 +23,7 @@ export type ParsedQuadbinTile = ParsedQuadbinCell( tile: Tile2DHeader>, + tileAggregationCache: Map[]>, aggregationLevels: number, properties: AggregationProperties = [], getPosition: Accessor, [number, number]>, @@ -32,7 +33,7 @@ export function aggregateTile( // Aggregate on demand and cache result if (!tile.userData) tile.userData = {}; - const cell0 = tile.userData[aggregationLevels]?.[0]; + const cell0 = tileAggregationCache.get(aggregationLevels)?.[0]; if (cell0) { // Have already aggregated this tile if (properties.every(property => property.name in cell0)) { @@ -41,7 +42,7 @@ export function aggregateTile( } // Aggregated properties have changed, re-aggregate - tile.userData = {}; + tileAggregationCache.clear(); } const out: Record = {}; @@ -49,7 +50,7 @@ export function aggregateTile( let id = cell.id; const position = typeof getPosition === 'function' ? getPosition(cell, {} as any) : getPosition; - // Aggregate by parent id + // Aggregate by parent rid for (let i = 0; i < aggregationLevels - 1; i++) { id = cellToParent(id); } @@ -93,7 +94,7 @@ export function aggregateTile( } } - tile.userData[aggregationLevels] = Object.values(out); + tileAggregationCache.set(aggregationLevels, Object.values(out)); return true; } diff --git a/modules/carto/src/sources/base-source.ts b/modules/carto/src/sources/base-source.ts index 45b31fbc0ce..e80f7ad5ab6 100644 --- a/modules/carto/src/sources/base-source.ts +++ b/modules/carto/src/sources/base-source.ts @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import {DEFAULT_API_BASE_URL, DEFAULT_CLIENT} from '../api/common'; +import {DEFAULT_API_BASE_URL, DEFAULT_CLIENT, DEFAULT_MAX_LENGTH_URL} from '../api/common'; import {buildSourceUrl} from '../api/endpoints'; import {requestWithParameters} from '../api/request-with-parameters'; import type {APIErrorContext, MapType} from '../api/types'; @@ -16,7 +16,8 @@ export const SOURCE_DEFAULTS: SourceOptionalOptions = { apiBaseUrl: DEFAULT_API_BASE_URL, clientId: DEFAULT_CLIENT, format: 'tilejson', - headers: {} + headers: {}, + maxLengthURL: DEFAULT_MAX_LENGTH_URL }; export async function baseSource>( @@ -32,7 +33,7 @@ export async function baseSource>( } } const baseUrl = buildSourceUrl(mergedOptions); - const {clientId, format} = mergedOptions; + const {clientId, maxLengthURL, format} = mergedOptions; const headers = {Authorization: `Bearer ${options.accessToken}`, ...options.headers}; const parameters = {client: clientId, ...urlParameters}; @@ -46,7 +47,8 @@ export async function baseSource>( baseUrl, parameters, headers, - errorContext + errorContext, + maxLengthURL }); const dataUrl = mapInstantiation[format].url[0]; @@ -59,7 +61,8 @@ export async function baseSource>( const json = await requestWithParameters({ baseUrl: dataUrl, headers, - errorContext + errorContext, + maxLengthURL }); if (accessToken) { json.accessToken = accessToken; @@ -70,6 +73,7 @@ export async function baseSource>( return await requestWithParameters({ baseUrl: dataUrl, headers, - errorContext + errorContext, + maxLengthURL }); } diff --git a/modules/carto/src/sources/types.ts b/modules/carto/src/sources/types.ts index 11786e1c2dd..a43b390298d 100644 --- a/modules/carto/src/sources/types.ts +++ b/modules/carto/src/sources/types.ts @@ -35,6 +35,13 @@ export type SourceOptionalOptions = { clientId: string; /** @deprecated use `query` instead **/ format: Format; + + /** + * Maximum URL character length. Above this limit, requests use POST. + * Used to avoid browser and CDN limits. + * @default {@link DEFAULT_MAX_LENGTH_URL} + */ + maxLengthURL?: number; }; export type SourceOptions = SourceRequiredOptions & Partial; diff --git a/modules/core/src/lib/deck-picker.ts b/modules/core/src/lib/deck-picker.ts index d3412226afb..0933934e276 100644 --- a/modules/core/src/lib/deck-picker.ts +++ b/modules/core/src/lib/deck-picker.ts @@ -159,7 +159,7 @@ export default class DeckPicker { } // Resize it to current canvas size (this is a noop if size hasn't changed) - const {canvas} = this.device.getCanvasContext(); + const {canvas} = this.device.getDefaultCanvasContext(); this.pickingFBO?.resize({width: canvas.width, height: canvas.height}); this.depthFBO?.resize({width: canvas.width, height: canvas.height}); } diff --git a/modules/core/src/lib/deck.ts b/modules/core/src/lib/deck.ts index 3b97d7100fc..74d29bee617 100644 --- a/modules/core/src/lib/deck.ts +++ b/modules/core/src/lib/deck.ts @@ -948,13 +948,15 @@ export default class Deck { // instrumentGLContext(this.device.gl, {enable: true, copyState: true}); } - this.device.setParametersWebGL({ - blend: true, - blendFunc: [GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA], - polygonOffsetFill: true, - depthTest: true, - depthFunc: GL.LEQUAL - }); + if (this.device instanceof WebGLDevice) { + this.device.setParametersWebGL({ + blend: true, + blendFunc: [GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA], + polygonOffsetFill: true, + depthTest: true, + depthFunc: GL.LEQUAL + }); + } this.props.onDeviceInitialized(this.device); if (this.device instanceof WebGLDevice) { diff --git a/modules/core/src/lib/layer.ts b/modules/core/src/lib/layer.ts index a07363796b9..9a3d75bf354 100644 --- a/modules/core/src/lib/layer.ts +++ b/modules/core/src/lib/layer.ts @@ -20,6 +20,7 @@ /* eslint-disable react/no-direct-mutation-state */ import {Buffer, TypedArray} from '@luma.gl/core'; +import {WebGLDevice} from '@luma.gl/webgl'; import {COORDINATE_SYSTEM} from './constants'; import AttributeManager from './attribute/attribute-manager'; import UniformTransitionManager from './uniform-transition-manager'; @@ -1164,14 +1165,27 @@ export default abstract class Layer extends Component< const {getPolygonOffset} = this.props; const offsets = (getPolygonOffset && getPolygonOffset(uniforms)) || [0, 0]; - context.device.setParametersWebGL({polygonOffset: offsets}); + if (context.device instanceof WebGLDevice) { + context.device.setParametersWebGL({polygonOffset: offsets}); + } for (const model of this.getModels()) { model.setParameters(parameters); } // Call subclass lifecycle method - context.device.withParametersWebGL(parameters, () => { + if (context.device instanceof WebGLDevice) { + context.device.withParametersWebGL(parameters, () => { + const opts = {renderPass, moduleParameters, uniforms, parameters, context}; + + // extensions + for (const extension of this.props.extensions) { + extension.draw.call(this, opts, extension); + } + + this.draw(opts); + }); + } else { const opts = {renderPass, moduleParameters, uniforms, parameters, context}; // extensions @@ -1180,7 +1194,7 @@ export default abstract class Layer extends Component< } this.draw(opts); - }); + } } finally { this.props = currentProps; } diff --git a/modules/extensions/src/data-filter/aggregator.ts b/modules/extensions/src/data-filter/aggregator.ts index 14d2333b267..80d66ea5d67 100644 --- a/modules/extensions/src/data-filter/aggregator.ts +++ b/modules/extensions/src/data-filter/aggregator.ts @@ -1,6 +1,5 @@ -import {Device, DeviceFeature, Framebuffer} from '@luma.gl/core'; -import {Model} from '@luma.gl/engine'; -import {GL} from '@luma.gl/constants'; +import {Device, DeviceFeature, Framebuffer, RenderPipelineParameters} from '@luma.gl/core'; +import {Model, ModelProps} from '@luma.gl/engine'; const AGGREGATE_VS = `\ #version 300 es @@ -84,7 +83,12 @@ export function getFramebuffer(device: Device, useFloatTarget: boolean): Framebu } // Increments the counter based on dataFilter_value -export function getModel(device: Device, shaderOptions: any, useFloatTarget: boolean): Model { +export function getModel( + device: Device, + bufferLayout: ModelProps['bufferLayout'], + shaderOptions: any, + useFloatTarget: boolean +): Model { shaderOptions.defines.NON_INSTANCED_MODEL = 1; if (useFloatTarget) { shaderOptions.defines.FLOAT_TARGET = 1; @@ -94,16 +98,21 @@ export function getModel(device: Device, shaderOptions: any, useFloatTarget: boo id: 'data-filter-aggregation-model', vertexCount: 1, isInstanced: false, - drawMode: GL.POINTS, + topology: 'point-list', vs: AGGREGATE_VS, fs: AGGREGATE_FS, + bufferLayout, ...shaderOptions }); } -export const parameters = { +export const parameters: RenderPipelineParameters = { blend: true, - blendFunc: [GL.ONE, GL.ONE, GL.ONE, GL.ONE], - blendEquation: [GL.FUNC_ADD, GL.FUNC_ADD], - depthTest: false + blendColorSrcFactor: 'one', + blendColorDstFactor: 'one', + blendAlphaSrcFactor: 'one', + blendAlphaDstFactor: 'one', + blendColorOperation: 'add', + blendAlphaOperation: 'add', + depthCompare: 'never' } as const; diff --git a/modules/extensions/src/data-filter/data-filter-extension.ts b/modules/extensions/src/data-filter/data-filter-extension.ts index 269afe67ebe..768ae8aea44 100644 --- a/modules/extensions/src/data-filter/data-filter-extension.ts +++ b/modules/extensions/src/data-filter/data-filter-extension.ts @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import type {Framebuffer} from '@luma.gl/core'; +import type {Buffer, Framebuffer} from '@luma.gl/core'; import type {Model} from '@luma.gl/engine'; import type {Layer, LayerContext, Accessor, UpdateParameters} from '@deck.gl/core'; import {_deepEqual as deepEqual, LayerExtension, log} from '@deck.gl/core'; @@ -30,6 +30,7 @@ import { dataFilter64 } from './shader-module'; import * as aggregator from './aggregator'; +import {NumberArray4} from '@math.gl/core'; const defaultProps = { getFilterValue: {type: 'accessor', value: 0}, @@ -204,7 +205,7 @@ export default class DataFilterExtension extends LayerExtension< // The vertex shader checks if a vertex has the same "index" as the previous vertex // so that we only write one count cross multiple vertices of the same object attributeManager.add({ - filterIndices: { + filterVertexIndices: { size: useFloatTarget ? 1 : 2, vertexOffset: 1, type: 'unorm8', @@ -226,6 +227,7 @@ export default class DataFilterExtension extends LayerExtension< const filterFBO = aggregator.getFramebuffer(device, useFloatTarget); const filterModel = aggregator.getModel( device, + attributeManager.getBufferLayouts({isInstanced: false}), extension.getShaders.call(this, extension), useFloatTarget ); @@ -311,32 +313,35 @@ export default class DataFilterExtension extends LayerExtension< /* eslint-disable-next-line camelcase */ if (filterNeedsUpdate && onFilteredItemsChange && filterModel) { + const attributeManager = this.getAttributeManager()!; const { - attributes: {filterValues, filterCategoryValues, filterIndices} - } = this.getAttributeManager()!; + attributes: {filterValues, filterCategoryValues, filterVertexIndices} + } = attributeManager; filterModel.setVertexCount(this.getNumInstances()); - this.context.device.clearWebGL({framebuffer: filterFBO, color: [0, 0, 0, 0]}); - - filterModel.updateModuleSettings(params.moduleParameters); - // @ts-expect-error filterValue and filterIndices should always have buffer value - filterModel.setAttributes({ + // @ts-expect-error filterValue and filterVertexIndices should always have buffer value + const attributes: Record = { ...filterValues?.getValue(), ...filterCategoryValues?.getValue(), - ...filterIndices?.getValue() + ...filterVertexIndices?.getValue() + }; + filterModel.setAttributes(attributes); + filterModel.shaderInputs.setProps({ + dataFilter: dataFilterProps }); - filterModel.shaderInputs.setProps({dataFilter: dataFilterProps}); - filterModel.device.withParametersWebGL( - { - framebuffer: filterFBO, - // ts-ignore 'readonly' cannot be assigned to the mutable type '[GLBlendEquation, GLBlendEquation]' - ...(aggregator.parameters as any), - viewport: [0, 0, filterFBO.width, filterFBO.height] - }, - () => { - filterModel.draw(this.context.renderPass); - } - ); + + const viewport = [0, 0, filterFBO.width, filterFBO.height] as NumberArray4; + + const renderPass = filterModel.device.beginRenderPass({ + id: 'data-filter-aggregation', + framebuffer: filterFBO, + parameters: {viewport}, + clearColor: [0, 0, 0, 0] + }); + filterModel.setParameters(aggregator.parameters); + filterModel.draw(renderPass); + renderPass.end(); + const color = filterModel.device.readPixelsToArrayWebGL(filterFBO); let count = 0; for (let i = 0; i < color.length; i++) { diff --git a/modules/google-maps/src/google-maps-overlay.ts b/modules/google-maps/src/google-maps-overlay.ts index c6e5450838a..68ae13c77d0 100644 --- a/modules/google-maps/src/google-maps-overlay.ts +++ b/modules/google-maps/src/google-maps-overlay.ts @@ -1,5 +1,6 @@ /* global google */ import {GL, GLParameters} from '@luma.gl/constants'; +import {WebGLDevice} from '@luma.gl/webgl'; import { createDeckInstance, destroyDeckInstance, @@ -257,14 +258,16 @@ export default class GoogleMapsOverlay { // As an optimization, some renders are to an separate framebuffer // which we need to pass onto deck - const _framebuffer = device.getParametersWebGL(GL.FRAMEBUFFER_BINDING); + if (device instanceof WebGLDevice) { + const _framebuffer = device.getParametersWebGL(GL.FRAMEBUFFER_BINDING); + } // @ts-expect-error deck.setProps({_framebuffer}); // With external gl context, animation loop doesn't resize webgl-canvas and thus fails to // calculate corrext pixel ratio. Force this manually. - device.getCanvasContext().resize(); + device.getDefaultCanvasContext().resize(); // Camera changed, will trigger a map repaint right after this // Clear any change flag triggered by setting viewState so that deck does not request @@ -273,17 +276,19 @@ export default class GoogleMapsOverlay { // Workaround for bug in Google maps where viewport state is wrong // TODO remove once fixed - device.setParametersWebGL({ - viewport: [0, 0, gl.canvas.width, gl.canvas.height], - scissor: [0, 0, gl.canvas.width, gl.canvas.height], - stencilFunc: [gl.ALWAYS, 0, 255, gl.ALWAYS, 0, 255] - }); + if (device instanceof WebGLDevice) { + device.setParametersWebGL({ + viewport: [0, 0, gl.canvas.width, gl.canvas.height], + scissor: [0, 0, gl.canvas.width, gl.canvas.height], + stencilFunc: [gl.ALWAYS, 0, 255, gl.ALWAYS, 0, 255] + }); - device.withParametersWebGL(GL_STATE, () => { - deck._drawLayers('google-vector', { - clearCanvas: false + device.withParametersWebGL(GL_STATE, () => { + deck._drawLayers('google-vector', { + clearCanvas: false + }); }); - }); + } } } diff --git a/modules/test-utils/src/snapshot-test-runner.ts b/modules/test-utils/src/snapshot-test-runner.ts index 1d015d1e7d8..ed88900b883 100644 --- a/modules/test-utils/src/snapshot-test-runner.ts +++ b/modules/test-utils/src/snapshot-test-runner.ts @@ -33,6 +33,7 @@ type ImageDiffOptions = { tolerance?: number; // 0.1, includeAA?: boolean; // false, includeEmpty?: boolean; // true + platform?: string; }; type DiffImageResult = { @@ -140,8 +141,20 @@ export class SnapshotTestRunner extends TestRunner< }; // Take screenshot and compare const result = await window.browserTestDriver_captureAndDiffScreen(diffOptions); + + // If failed, try if we have a platform specific golden image + let resultOverride; + const platform = this.testOptions.imageDiffOptions?.platform?.toLowerCase(); + if (!result.success) { + diffOptions.goldenImage = diffOptions.goldenImage.replace( + 'golden-images/', + `golden-images/platform-overrides/${platform}/` + ); + resultOverride = await window.browserTestDriver_captureAndDiffScreen(diffOptions); + } + // invoke user callback - if (result.success) { + if (result.success || resultOverride.success) { this.pass(result); } else { this.fail(result); diff --git a/test/apps/collision/app.jsx b/test/apps/collision/app.jsx index 01d00003be1..6a63f9ecdb2 100644 --- a/test/apps/collision/app.jsx +++ b/test/apps/collision/app.jsx @@ -8,7 +8,8 @@ import {GeoJsonLayer, SolidPolygonLayer, TextLayer} from '@deck.gl/layers'; import {CollisionFilterExtension, MaskExtension} from '@deck.gl/extensions'; import {VectorTileLayer, vectorTableSource} from '@deck.gl/carto'; -const accessToken = 'XXX'; +const accessToken = + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfbHFlM3p3Z3UiLCJqdGkiOiJkOTU4OWMyZiJ9.78MdzU2J6y-J6Far71_Mh7IQO9eYIZD9nECUiZJAVL4'; const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json'; const AIR_PORTS = 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson'; @@ -19,8 +20,8 @@ const US_STATES = const cartoData = vectorTableSource({ accessToken, - connectionName: 'bigquery', - tableName: 'cartobq.public_account.populated_places' + connectionName: 'carto_dw', + tableName: 'carto-demo-data.demo_tables.populated_places' }); /* eslint-disable react/no-deprecated */ diff --git a/test/apps/collision/package.json b/test/apps/collision/package.json index 493d558bc3a..4c57eddb561 100644 --- a/test/apps/collision/package.json +++ b/test/apps/collision/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "deck.gl": "^8.6.0-alpha.0", + "maplibre-gl": "^3.0.0", "react": "^16.3.0", "react-dom": "^16.3.0", "react-map-gl": "^7.1.0" diff --git a/test/modules/carto/api/request-with-parameters.spec.ts b/test/modules/carto/api/request-with-parameters.spec.ts index f17c7303d6a..0624b2a35b3 100644 --- a/test/modules/carto/api/request-with-parameters.spec.ts +++ b/test/modules/carto/api/request-with-parameters.spec.ts @@ -192,3 +192,36 @@ test('requestWithParameters#precedence', async t => { }); t.end(); }); + +test('requestWithParameters#maxLengthURL', async t => { + await withMockFetchMapsV3(async calls => { + t.equals(calls.length, 0, '0 initial calls'); + + await Promise.all([ + requestWithParameters({ + baseUrl: 'https://example.com/v1/item/1' + }), + requestWithParameters({ + baseUrl: 'https://example.com/v1/item/2', + maxLengthURL: 10 + }), + requestWithParameters({ + baseUrl: `https://example.com/v1/item/3`, + parameters: {content: 'long'.padEnd(10_000, 'g')} // > default limit + }), + requestWithParameters({ + baseUrl: `https://example.com/v1/item/4`, + parameters: {content: 'long'.padEnd(10_000, 'g')}, + maxLengthURL: 15_000 + }) + ]); + + t.equals(calls.length, 4, '4 requests'); + t.deepEquals( + calls.map(({method}) => method ?? 'GET'), + ['GET', 'POST', 'POST', 'GET'], + 'request method' + ); + }); + t.end(); +}); diff --git a/test/modules/extensions/data-filter.spec.ts b/test/modules/extensions/data-filter.spec.ts index 1491a6eed6a..c307ec568ff 100644 --- a/test/modules/extensions/data-filter.spec.ts +++ b/test/modules/extensions/data-filter.spec.ts @@ -140,6 +140,7 @@ test('DataFilterExtension#categories', t => { test('DataFilterExtension#countItems', t => { let cbCalled = 0; + let cbCount = -1; const testCases = [ { @@ -150,12 +151,16 @@ test('DataFilterExtension#countItems', t => { ], getPosition: d => d.position, getFilterValue: d => d.timestamp, - onFilteredItemsChange: () => cbCalled++, + onFilteredItemsChange: event => { + cbCalled++; + cbCount = event.count; + }, filterRange: [80, 160], extensions: [new DataFilterExtension({filterSize: 1, countItems: true})] }, onAfterUpdate: () => { t.is(cbCalled, 1, 'onFilteredItemsChange is called'); + t.is(cbCount, 2, 'count is correct'); } }, { @@ -172,6 +177,7 @@ test('DataFilterExtension#countItems', t => { }, onAfterUpdate: () => { t.is(cbCalled, 2, 'onFilteredItemsChange is called'); + t.is(cbCount, 0, 'count is correct'); } } ]; diff --git a/test/render/constants.js b/test/render/constants.ts similarity index 100% rename from test/render/constants.js rename to test/render/constants.ts diff --git a/test/render/golden-images/platform-overrides/mac/arc-lnglat.png b/test/render/golden-images/platform-overrides/mac/arc-lnglat.png new file mode 100644 index 00000000000..b0cf8d564c9 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/arc-lnglat.png differ diff --git a/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe-flatshading.png b/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe-flatshading.png new file mode 100644 index 00000000000..ada102bf06e Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe-flatshading.png differ diff --git a/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe-vertices.png b/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe-vertices.png new file mode 100644 index 00000000000..0cb2f0173f4 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe-vertices.png differ diff --git a/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe.png b/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe.png new file mode 100644 index 00000000000..85603620a87 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/column-lnglat-extruded-wireframe.png differ diff --git a/test/render/golden-images/platform-overrides/mac/contour-infoviz.png b/test/render/golden-images/platform-overrides/mac/contour-infoviz.png new file mode 100644 index 00000000000..648a5297a18 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/contour-infoviz.png differ diff --git a/test/render/golden-images/platform-overrides/mac/contour-lnglat.png b/test/render/golden-images/platform-overrides/mac/contour-lnglat.png new file mode 100644 index 00000000000..b8bd77afaf2 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/contour-lnglat.png differ diff --git a/test/render/golden-images/platform-overrides/mac/grid-lnglat-side.png b/test/render/golden-images/platform-overrides/mac/grid-lnglat-side.png new file mode 100644 index 00000000000..a80a8f65123 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/grid-lnglat-side.png differ diff --git a/test/render/golden-images/platform-overrides/mac/icon-lnglat-facing-up.png b/test/render/golden-images/platform-overrides/mac/icon-lnglat-facing-up.png new file mode 100644 index 00000000000..da691a5beaf Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/icon-lnglat-facing-up.png differ diff --git a/test/render/golden-images/platform-overrides/mac/path-globe.png b/test/render/golden-images/platform-overrides/mac/path-globe.png new file mode 100644 index 00000000000..384cf6a62a0 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/path-globe.png differ diff --git a/test/render/golden-images/platform-overrides/mac/polygon-pattern-mask.png b/test/render/golden-images/platform-overrides/mac/polygon-pattern-mask.png new file mode 100644 index 00000000000..9f0a723dd30 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/polygon-pattern-mask.png differ diff --git a/test/render/golden-images/platform-overrides/mac/polygon-pattern.png b/test/render/golden-images/platform-overrides/mac/polygon-pattern.png new file mode 100644 index 00000000000..69d1d1a2b26 Binary files /dev/null and b/test/render/golden-images/platform-overrides/mac/polygon-pattern.png differ diff --git a/test/render/index.js b/test/render/index.js index be9f722a799..d43163e3fdc 100644 --- a/test/render/index.js +++ b/test/render/index.js @@ -19,7 +19,7 @@ // THE SOFTWARE. import test from 'tape'; import TEST_CASES from './test-cases'; -import {WIDTH, HEIGHT} from './constants'; +import {WIDTH, HEIGHT, OS} from './constants'; import {SnapshotTestRunner} from '@deck.gl/test-utils'; import './jupyter-widget'; @@ -40,6 +40,7 @@ test('Render Test', t => { timeout: 10000, imageDiffOptions: { + platform: OS, threshold: 0.99, includeEmpty: false // uncomment to save screenshot to disk diff --git a/website/static/events/london-summit-2024/index.html b/website/static/events/london-summit-2024/index.html new file mode 100644 index 00000000000..3b214b5976d --- /dev/null +++ b/website/static/events/london-summit-2024/index.html @@ -0,0 +1,533 @@ + + + + + + + Open Visualization Collaborator Summit 2024 + + + + + + +
+
+
+ +

Open Visualization
Collaborator Summit 2024

+
+
+
+ +
+
+
+
+
+

+ + + + + + + + Microsoft Reactor
+ 2 Kingdom Street
+ London W2 6BD +

+
+
+

+ Come together with the + vis.gl/deck.gl + community to discuss the present & future of the leading open source + library for geospatial applications. +

+
+
+ +
+

+ At the Open Visualization Collaborator Summit, we'll be bringing + together an international audience of geospatial minds to discuss how + they are using deck.gl to build apps, foster more contribution and + envisage the future of the leading open source mapping library. +

+ GET YOUR TICKET NOW +
+ +
+
+

Where?

+

+ We'll be hosting the Open Visualization Collaborator Summit in + the Microsoft Reactor space in Paddington, London. +

+
+
+ Microsoft Reactor London +
+
+ +
+ +
+ +
+
+ Gathering at Microsoft Reactor London +
+
+

Who?

+

+ We are contributors who maintain the open-source stack that powers deck.gl; + developers who build amazing applications with it; + and folks who are passionate about data visualization in general. + Come along to talk about graphics rendering in the browser, GPU-accelerated data processing, + and the Open Visualization mission. + Share your work, meet others facing + similar challenges, and discuss the future of the community. +

+
+
+ +
+

Agenda

+ +
+

+ Tuesday, September 10th +

+ +
+
+

08:30 Breakfast & Social

+
+
+
+
+

09:00 - 09:55

+
+ Xiaoji Chen +
+

+ vis.gl State of the Union +

+ +

+ Xiaoji Chen + Technical Steering Committee +

+
+
+ Microsoft +
+
+
+ +
+
+
+

10:00 - 10:25

+
+ Ib Green +
+

+ deck.gl on WebGPU +

+ +

+ Ib Green Technical Steering Committee +

+
+
+ Foursquare +
+
+
+ +
+

10:30 Break & Networking

+
+ +
+
+
+

11:00 - 11:25

+
+ Felix Palmer +
+

+ Transitioning from WebGL to WebGPU +

+ +

+ Felix Palmer Technical Steering Committee +

+
+
+ CARTO +
+
+
+ +
+
+
+

11:30 - 11:55

+
+ Kyle Barron +
+

+ deck.gl, GeoArrow and Lonboard +

+ +

Kyle Barron Cloud Engineer

+
+ +
+ Development Seed +
+
+
+ + +
+

12:00 Lunch

+
+ +
+
+
+

13:00 - 13:25

+
+ Brandon Pierce +
+

+ React Renderer for Deck.gl +

+ +

Brandon Pierce Principal Engineer

+
+ +
+ Hypergiant +
+
+
+ +
+
+
+

13:30 - 13:55

+
+ Paul Taylor +
+

+ NVIDIA RAPIDS: Speed of Light ETL for Geospatial Vis +

+ +

Paul Taylor

+
+ +
+ NVIDIA +
+
+
+ +
+
+
+

14:00 - 14:25

+
+ Sergey Sukov +
+

+ Loaders.gl - latest features and 2024 roadmap +

+ +

+ Sergey Sukov Founder and CEO @ ActionEngine +

+
+
+ Action Engine +
+
+
+ +
+

14:30 Tea and networking 🫖

+
+ +
+
+
+

15:00 - 15:25

+
+ Don McCurdy +
+

+ vis.gl and three.js - Architecture and ecosystem comparisons +

+ +

+ Don McCurdy vis.gl contributor +

+
+
+ CARTO +
+
+
+ +
+
+
+

15:30 - 16:00

+
+ TSC +
+

+ Panel: Where do we take vis.gl in the next 5 years? +

+ +

+ Technical Steering Committee +

+
+
+
+
+
+ +
+
+ +
+

+ Wednesday, September 11th +

+ +
+
+

09:00 Breakfast & Social

+
+ +
+
+
+

09:30 - 10:10

+
+ Juan Carlos Alonso + Jose Angel Parreño + Javier Abia +
+

+ Deck migration log book +

+ +

Juan Carlos Alonso

+

Jose Angel Parreño

+

Javier Abia Front End team

+
+
+ Global Fishing Watch +
+
+
+ +
+
+
+

10:15 - 10:55

+
+ Oliver Wipfli +
+

+ MapLibre Text Rendering With WebGL +

+ +

+ Oliver Wipfli Independent Consultant +

+
+
+
+ +
+
+
+

11:00 - 11:25

+
+ Chris Gervang + Adam Krebs +
+

+ Procedural Content Creation with deck.gl +

+ +

+ Chris Gervang Technical Steering Committee + Adam Krebs Creative Technical Director @ Joby Aviation +

+
+
+ Joby +
+
+
+ +
+
+
+

11:30 - 11:55

+
+ Chris Kapp +
+

+ Going Global with 3D Tiles +

+ +

+ Chris Kapp Senior Software Engineer +

+
+
+ Sensat +
+
+
+ +
+

12:00 Lunch & Networking

+
+ +
+
+
+

13:00 - 13:25

+
+ Harel Mazor +
+

+ Captain Hook - Mastering MapLibre's Hooks +

+ +

+ Harel Mazor Open-Source Advocator +

+
+
+
+ +
+
+
+

13:30 - 13:55

+
+ Ilya Boyandin +
+

+ Real-time Collaborative Map Drawing with deck.gl +

+ +

+ Ilya Boyandin +

+
+
+ Foursquare +
+
+
+ +
+
+
+

14:00 - 14:25

+
+ Jan Žák +
+

+ WeatherLayers - Weather Visualization with deck.gl +

+ +

+ Jan Žák Software Engineer / Consultant +

+
+
+
+ +
+
+
+

14:30 - 15:00

+
+ TSC +
+

+ Panel: How do we further empower the applications in the ecosystem? +

+ +

+ Technical Steering Committee +

+
+
+
+
+
+ +
+
+
+
+
+ +
+ + + + + diff --git a/website/static/events/london-summit-2024/style.css b/website/static/events/london-summit-2024/style.css new file mode 100644 index 00000000000..4e414acbed1 --- /dev/null +++ b/website/static/events/london-summit-2024/style.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body,html{height:100%}body{color:#2e3c43;font-family:"Open sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:20px;line-height:32px;-webkit-font-smoothing:antialiased;-webkit-text-rendering:optimizeLegibility;text-rendering:optimizeLegibility}a{color:#1785fb;text-decoration:none}a:hover{text-decoration:underline}button{border:0;cursor:pointer;font-family:"Open sans","Helvetica Neue",Helvetica,Arial,sans-serif;margin:0;outline:0}iframe,img{max-width:100%;vertical-align:bottom}@media (max-width:600px){iframe{width:1px;min-width:100%}}sup{font-size:85%;position:relative;top:-6px}a,b,body,center,div,embed,footer,h1,h2,h3,h4,h5,h6,header,html,i,iframe,img,p,s,section,sup,table,time,u{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}footer,header,section{display:block}body{line-height:1}table{border-collapse:collapse;border-spacing:0}.f84{font-size:84px;line-height:96px;letter-spacing:-1px}.f64{font-size:64px;line-height:72px;letter-spacing:-.5px}.f48{font-size:48px;line-height:56px;letter-spacing:-.3px}.f36{font-size:36px;line-height:44px;letter-spacing:-.2px}.f32{font-size:32px;line-height:40px;letter-spacing:-.1px}.f24{font-size:24px;line-height:32px;letter-spacing:0}.f20{font-size:20px;line-height:28px;letter-spacing:0}.f18{font-size:18px;line-height:26px;letter-spacing:0}.f16{font-size:16px;line-height:24px;letter-spacing:0}.f12{font-size:12px;line-height:16px;letter-spacing:0}@media (max-width:600px){.f84--mobile{font-size:84px;line-height:96px;letter-spacing:-1px}.f64--mobile{font-size:64px;line-height:72px;letter-spacing:-.5px}.f48--mobile{font-size:48px;line-height:56px;letter-spacing:-.3px}.f36--mobile{font-size:36px;line-height:44px;letter-spacing:-.2px}.f32--mobile{font-size:32px;line-height:40px;letter-spacing:-.1px}.f24--mobile{font-size:24px;line-height:32px;letter-spacing:0}.f20--mobile{font-size:20px;line-height:28px;letter-spacing:0}.f18--mobile{font-size:18px;line-height:26px;letter-spacing:0}.f16--mobile{font-size:16px;line-height:24px;letter-spacing:0}.f12--mobile{font-size:12px;line-height:16px;letter-spacing:0}}@media (max-width:800px){.f84--tablet{font-size:84px;line-height:96px;letter-spacing:-1px}.f64--tablet{font-size:64px;line-height:72px;letter-spacing:-.5px}.f48--tablet{font-size:48px;line-height:56px;letter-spacing:-.3px}.f36--tablet{font-size:36px;line-height:44px;letter-spacing:-.2px}.f32--tablet{font-size:32px;line-height:40px;letter-spacing:-.1px}.f24--tablet{font-size:24px;line-height:32px;letter-spacing:0}.f20--tablet{font-size:20px;line-height:28px;letter-spacing:0}.f18--tablet{font-size:18px;line-height:26px;letter-spacing:0}.f16--tablet{font-size:16px;line-height:24px;letter-spacing:0}.f12--tablet{font-size:12px;line-height:16px;letter-spacing:0}}@media (max-width:1224px){.f84--desktop-s{font-size:84px;line-height:96px;letter-spacing:-1px}.f64--desktop-s{font-size:64px;line-height:72px;letter-spacing:-.5px}.f48--desktop-s{font-size:48px;line-height:56px;letter-spacing:-.3px}.f36--desktop-s{font-size:36px;line-height:44px;letter-spacing:-.2px}.f32--desktop-s{font-size:32px;line-height:40px;letter-spacing:-.1px}.f24--desktop-s{font-size:24px;line-height:32px;letter-spacing:0}.f20--desktop-s{font-size:20px;line-height:28px;letter-spacing:0}.f18--desktop-s{font-size:18px;line-height:26px;letter-spacing:0}.f16--desktop-s{font-size:16px;line-height:24px;letter-spacing:0}.f12--desktop-s{font-size:12px;line-height:16px;letter-spacing:0}}@media (min-width:1632px){.f84--desktop-l{font-size:84px;line-height:96px;letter-spacing:-1px}.f64--desktop-l{font-size:64px;line-height:72px;letter-spacing:-.5px}.f48--desktop-l{font-size:48px;line-height:56px;letter-spacing:-.3px}.f36--desktop-l{font-size:36px;line-height:44px;letter-spacing:-.2px}.f32--desktop-l{font-size:32px;line-height:40px;letter-spacing:-.1px}.f24--desktop-l{font-size:24px;line-height:32px;letter-spacing:0}.f20--desktop-l{font-size:20px;line-height:28px;letter-spacing:0}.f18--desktop-l{font-size:18px;line-height:26px;letter-spacing:0}.f16--desktop-l{font-size:16px;line-height:24px;letter-spacing:0}.f12--desktop-l{font-size:12px;line-height:16px;letter-spacing:0}}.title{font-family:Montserrat,"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:600}.is-bgWhite{background:#fff}.is-txtWhite{color:#fff}.container{max-width:960px;margin:0 auto}.grid-cell{padding-right:12px;padding-left:12px}.grid{display:flex;flex-wrap:wrap;flex:1}.grid.grid--column{flex-direction:column}@media (max-width:1224px){.container{max-width:960px}.grid-cell{padding-right:12px;padding-left:12px}}@media (max-width:600px){.grid-cell{padding-right:4px;padding-left:4px}}@media (min-width:1224px){.container{max-width:1224px}.grid-cell{padding-right:12px;padding-left:12px}}@media (min-width:1632px){.container{max-width:1632px}.grid-cell{padding-right:16px;padding-left:16px}}.grid-cell--col0{flex:0 0 0%;max-width:0%}.grid-cell--col1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.grid-cell--col2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.grid-cell--col3{flex:0 0 25%;max-width:25%}.grid-cell--col4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.grid-cell--col5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.grid-cell--col6{flex:0 0 50%;max-width:50%}.grid-cell--col7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.grid-cell--col8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.grid-cell--col9{flex:0 0 75%;max-width:75%}.grid-cell--col10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.grid-cell--col11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.grid-cell--col12{flex:0 0 100%;max-width:100%}@media (max-width:1224px){.grid-cell--col0--desktop-s{flex:0 0 0%;max-width:0%}.grid-cell--col1--desktop-s{flex:0 0 8.3333333333%;max-width:8.3333333333%}.grid-cell--col2--desktop-s{flex:0 0 16.6666666667%;max-width:16.6666666667%}.grid-cell--col3--desktop-s{flex:0 0 25%;max-width:25%}.grid-cell--col4--desktop-s{flex:0 0 33.3333333333%;max-width:33.3333333333%}.grid-cell--col5--desktop-s{flex:0 0 41.6666666667%;max-width:41.6666666667%}.grid-cell--col6--desktop-s{flex:0 0 50%;max-width:50%}.grid-cell--col7--desktop-s{flex:0 0 58.3333333333%;max-width:58.3333333333%}.grid-cell--col8--desktop-s{flex:0 0 66.6666666667%;max-width:66.6666666667%}.grid-cell--col9--desktop-s{flex:0 0 75%;max-width:75%}.grid-cell--col10--desktop-s{flex:0 0 83.3333333333%;max-width:83.3333333333%}.grid-cell--col11--desktop-s{flex:0 0 91.6666666667%;max-width:91.6666666667%}.grid-cell--col12--desktop-s{flex:0 0 100%;max-width:100%}.grid-cell--col0--desktop-s{padding:0}}@media (max-width:800px){.grid-cell--col0--tablet{flex:0 0 0%;max-width:0%}.grid-cell--col1--tablet{flex:0 0 8.3333333333%;max-width:8.3333333333%}.grid-cell--col2--tablet{flex:0 0 16.6666666667%;max-width:16.6666666667%}.grid-cell--col3--tablet{flex:0 0 25%;max-width:25%}.grid-cell--col4--tablet{flex:0 0 33.3333333333%;max-width:33.3333333333%}.grid-cell--col5--tablet{flex:0 0 41.6666666667%;max-width:41.6666666667%}.grid-cell--col6--tablet{flex:0 0 50%;max-width:50%}.grid-cell--col7--tablet{flex:0 0 58.3333333333%;max-width:58.3333333333%}.grid-cell--col8--tablet{flex:0 0 66.6666666667%;max-width:66.6666666667%}.grid-cell--col9--tablet{flex:0 0 75%;max-width:75%}.grid-cell--col10--tablet{flex:0 0 83.3333333333%;max-width:83.3333333333%}.grid-cell--col11--tablet{flex:0 0 91.6666666667%;max-width:91.6666666667%}.grid-cell--col12--tablet{flex:0 0 100%;max-width:100%}.grid-cell--col0--tablet{padding:0}}@media (max-width:600px){.grid-cell--col0--mobile{flex:0 0 0%;max-width:0%}.grid-cell--col1--mobile{flex:0 0 8.3333333333%;max-width:8.3333333333%}.grid-cell--col2--mobile{flex:0 0 16.6666666667%;max-width:16.6666666667%}.grid-cell--col3--mobile{flex:0 0 25%;max-width:25%}.grid-cell--col4--mobile{flex:0 0 33.3333333333%;max-width:33.3333333333%}.grid-cell--col5--mobile{flex:0 0 41.6666666667%;max-width:41.6666666667%}.grid-cell--col6--mobile{flex:0 0 50%;max-width:50%}.grid-cell--col7--mobile{flex:0 0 58.3333333333%;max-width:58.3333333333%}.grid-cell--col8--mobile{flex:0 0 66.6666666667%;max-width:66.6666666667%}.grid-cell--col9--mobile{flex:0 0 75%;max-width:75%}.grid-cell--col10--mobile{flex:0 0 83.3333333333%;max-width:83.3333333333%}.grid-cell--col11--mobile{flex:0 0 91.6666666667%;max-width:91.6666666667%}.grid-cell--col12--mobile{flex:0 0 100%;max-width:100%}.grid-cell--col0--mobile{padding:0}}@media (min-width:1224px){.grid-cell--col0--desktop{flex:0 0 0%;max-width:0%}.grid-cell--col1--desktop{flex:0 0 8.3333333333%;max-width:8.3333333333%}.grid-cell--col2--desktop{flex:0 0 16.6666666667%;max-width:16.6666666667%}.grid-cell--col3--desktop{flex:0 0 25%;max-width:25%}.grid-cell--col4--desktop{flex:0 0 33.3333333333%;max-width:33.3333333333%}.grid-cell--col5--desktop{flex:0 0 41.6666666667%;max-width:41.6666666667%}.grid-cell--col6--desktop{flex:0 0 50%;max-width:50%}.grid-cell--col7--desktop{flex:0 0 58.3333333333%;max-width:58.3333333333%}.grid-cell--col8--desktop{flex:0 0 66.6666666667%;max-width:66.6666666667%}.grid-cell--col9--desktop{flex:0 0 75%;max-width:75%}.grid-cell--col10--desktop{flex:0 0 83.3333333333%;max-width:83.3333333333%}.grid-cell--col11--desktop{flex:0 0 91.6666666667%;max-width:91.6666666667%}.grid-cell--col12--desktop{flex:0 0 100%;max-width:100%}.grid-cell--col0--desktop{padding:0}}@media (min-width:1632px){.grid-cell--col0--desktop-l{flex:0 0 0%;max-width:0%}.grid-cell--col1--desktop-l{flex:0 0 8.3333333333%;max-width:8.3333333333%}.grid-cell--col2--desktop-l{flex:0 0 16.6666666667%;max-width:16.6666666667%}.grid-cell--col3--desktop-l{flex:0 0 25%;max-width:25%}.grid-cell--col4--desktop-l{flex:0 0 33.3333333333%;max-width:33.3333333333%}.grid-cell--col5--desktop-l{flex:0 0 41.6666666667%;max-width:41.6666666667%}.grid-cell--col6--desktop-l{flex:0 0 50%;max-width:50%}.grid-cell--col7--desktop-l{flex:0 0 58.3333333333%;max-width:58.3333333333%}.grid-cell--col8--desktop-l{flex:0 0 66.6666666667%;max-width:66.6666666667%}.grid-cell--col9--desktop-l{flex:0 0 75%;max-width:75%}.grid-cell--col10--desktop-l{flex:0 0 83.3333333333%;max-width:83.3333333333%}.grid-cell--col11--desktop-l{flex:0 0 91.6666666667%;max-width:91.6666666667%}.grid-cell--col12--desktop-l{flex:0 0 100%;max-width:100%}.grid-cell--col0--desktop-l{padding:0}}.section{overflow:hidden}.button{display:inline-block;border-radius:8px;font:600 16px/24px Montserrat,"Helvetica Neue",Helvetica,Arial,sans-serif;padding:16px 24px;text-align:center;white-space:nowrap;position:relative;overflow:hidden}.button:before{content:'';position:absolute;width:100%;height:100%;left:0;top:0;background-color:transparent;transition:background-color .1s linear}.button:hover{text-decoration:none}.features__title{display:inline-block;color:#c5ccd5;padding-top:12px;border-top:1px solid rgba(197,204,213,.2)}.u-mt0{margin-top:0}.u-mr0{margin-right:0}.u-mb0{margin-bottom:0}.u-mt4{margin-top:4px}.u-mr4{margin-right:4px}.u-mb4{margin-bottom:4px}.u-mt6{margin-top:6px}.u-mr6{margin-right:6px}.u-mb6{margin-bottom:6px}.u-mt8{margin-top:8px}.u-mr8{margin-right:8px}.u-mb8{margin-bottom:8px}.u-mt10{margin-top:10px}.u-mr10{margin-right:10px}.u-mb10{margin-bottom:10px}.u-mt12{margin-top:12px}.u-mr12{margin-right:12px}.u-mb12{margin-bottom:12px}.u-mt16{margin-top:16px}.u-mr16{margin-right:16px}.u-mb16{margin-bottom:16px}.u-mt20{margin-top:20px}.u-mr20{margin-right:20px}.u-mb20{margin-bottom:20px}.u-mt24{margin-top:24px}.u-mr24{margin-right:24px}.u-mb24{margin-bottom:24px}.u-mt28{margin-top:28px}.u-mr28{margin-right:28px}.u-mb28{margin-bottom:28px}.u-mt32{margin-top:32px}.u-mr32{margin-right:32px}.u-mb32{margin-bottom:32px}.u-mt36{margin-top:36px}.u-mr36{margin-right:36px}.u-mb36{margin-bottom:36px}.u-mt40{margin-top:40px}.u-mr40{margin-right:40px}.u-mb40{margin-bottom:40px}.u-mt48{margin-top:48px}.u-mr48{margin-right:48px}.u-mb48{margin-bottom:48px}.u-mt56{margin-top:56px}.u-mr56{margin-right:56px}.u-mb56{margin-bottom:56px}.u-mt64{margin-top:64px}.u-mr64{margin-right:64px}.u-mb64{margin-bottom:64px}.u-mt72{margin-top:72px}.u-mr72{margin-right:72px}.u-mb72{margin-bottom:72px}.u-mt80{margin-top:80px}.u-mr80{margin-right:80px}.u-mb80{margin-bottom:80px}.u-mt96{margin-top:96px}.u-mr96{margin-right:96px}.u-mb96{margin-bottom:96px}.u-mt120{margin-top:120px}.u-mr120{margin-right:120px}.u-mb120{margin-bottom:120px}.u-mt128{margin-top:128px}.u-mr128{margin-right:128px}.u-mb128{margin-bottom:128px}.u-mt148{margin-top:148px}.u-mr148{margin-right:148px}.u-mb148{margin-bottom:148px}@media (max-width:600px){.u-mt0--mobile{margin-top:0}.u-mr0--mobile{margin-right:0}.u-mb0--mobile{margin-bottom:0}.u-mt4--mobile{margin-top:4px}.u-mr4--mobile{margin-right:4px}.u-mb4--mobile{margin-bottom:4px}.u-mt6--mobile{margin-top:6px}.u-mr6--mobile{margin-right:6px}.u-mb6--mobile{margin-bottom:6px}.u-mt8--mobile{margin-top:8px}.u-mr8--mobile{margin-right:8px}.u-mb8--mobile{margin-bottom:8px}.u-mt10--mobile{margin-top:10px}.u-mr10--mobile{margin-right:10px}.u-mb10--mobile{margin-bottom:10px}.u-mt12--mobile{margin-top:12px}.u-mr12--mobile{margin-right:12px}.u-mb12--mobile{margin-bottom:12px}.u-mt16--mobile{margin-top:16px}.u-mr16--mobile{margin-right:16px}.u-mb16--mobile{margin-bottom:16px}.u-mt20--mobile{margin-top:20px}.u-mr20--mobile{margin-right:20px}.u-mb20--mobile{margin-bottom:20px}.u-mt24--mobile{margin-top:24px}.u-mr24--mobile{margin-right:24px}.u-mb24--mobile{margin-bottom:24px}.u-mt28--mobile{margin-top:28px}.u-mr28--mobile{margin-right:28px}.u-mb28--mobile{margin-bottom:28px}.u-mt32--mobile{margin-top:32px}.u-mr32--mobile{margin-right:32px}.u-mb32--mobile{margin-bottom:32px}.u-mt36--mobile{margin-top:36px}.u-mr36--mobile{margin-right:36px}.u-mb36--mobile{margin-bottom:36px}.u-mt40--mobile{margin-top:40px}.u-mr40--mobile{margin-right:40px}.u-mb40--mobile{margin-bottom:40px}.u-mt48--mobile{margin-top:48px}.u-mr48--mobile{margin-right:48px}.u-mb48--mobile{margin-bottom:48px}.u-mt56--mobile{margin-top:56px}.u-mr56--mobile{margin-right:56px}.u-mb56--mobile{margin-bottom:56px}.u-mt64--mobile{margin-top:64px}.u-mr64--mobile{margin-right:64px}.u-mb64--mobile{margin-bottom:64px}.u-mt72--mobile{margin-top:72px}.u-mr72--mobile{margin-right:72px}.u-mb72--mobile{margin-bottom:72px}.u-mt80--mobile{margin-top:80px}.u-mr80--mobile{margin-right:80px}.u-mb80--mobile{margin-bottom:80px}.u-mt96--mobile{margin-top:96px}.u-mr96--mobile{margin-right:96px}.u-mb96--mobile{margin-bottom:96px}.u-mt120--mobile{margin-top:120px}.u-mr120--mobile{margin-right:120px}.u-mb120--mobile{margin-bottom:120px}.u-mt128--mobile{margin-top:128px}.u-mr128--mobile{margin-right:128px}.u-mb128--mobile{margin-bottom:128px}.u-mt148--mobile{margin-top:148px}.u-mr148--mobile{margin-right:148px}.u-mb148--mobile{margin-bottom:148px}}@media (max-width:800px){.u-mt0--tablet{margin-top:0}.u-mr0--tablet{margin-right:0}.u-mb0--tablet{margin-bottom:0}.u-mt4--tablet{margin-top:4px}.u-mr4--tablet{margin-right:4px}.u-mb4--tablet{margin-bottom:4px}.u-mt6--tablet{margin-top:6px}.u-mr6--tablet{margin-right:6px}.u-mb6--tablet{margin-bottom:6px}.u-mt8--tablet{margin-top:8px}.u-mr8--tablet{margin-right:8px}.u-mb8--tablet{margin-bottom:8px}.u-mt10--tablet{margin-top:10px}.u-mr10--tablet{margin-right:10px}.u-mb10--tablet{margin-bottom:10px}.u-mt12--tablet{margin-top:12px}.u-mr12--tablet{margin-right:12px}.u-mb12--tablet{margin-bottom:12px}.u-mt16--tablet{margin-top:16px}.u-mr16--tablet{margin-right:16px}.u-mb16--tablet{margin-bottom:16px}.u-mt20--tablet{margin-top:20px}.u-mr20--tablet{margin-right:20px}.u-mb20--tablet{margin-bottom:20px}.u-mt24--tablet{margin-top:24px}.u-mr24--tablet{margin-right:24px}.u-mb24--tablet{margin-bottom:24px}.u-mt28--tablet{margin-top:28px}.u-mr28--tablet{margin-right:28px}.u-mb28--tablet{margin-bottom:28px}.u-mt32--tablet{margin-top:32px}.u-mr32--tablet{margin-right:32px}.u-mb32--tablet{margin-bottom:32px}.u-mt36--tablet{margin-top:36px}.u-mr36--tablet{margin-right:36px}.u-mb36--tablet{margin-bottom:36px}.u-mt40--tablet{margin-top:40px}.u-mr40--tablet{margin-right:40px}.u-mb40--tablet{margin-bottom:40px}.u-mt48--tablet{margin-top:48px}.u-mr48--tablet{margin-right:48px}.u-mb48--tablet{margin-bottom:48px}.u-mt56--tablet{margin-top:56px}.u-mr56--tablet{margin-right:56px}.u-mb56--tablet{margin-bottom:56px}.u-mt64--tablet{margin-top:64px}.u-mr64--tablet{margin-right:64px}.u-mb64--tablet{margin-bottom:64px}.u-mt72--tablet{margin-top:72px}.u-mr72--tablet{margin-right:72px}.u-mb72--tablet{margin-bottom:72px}.u-mt80--tablet{margin-top:80px}.u-mr80--tablet{margin-right:80px}.u-mb80--tablet{margin-bottom:80px}.u-mt96--tablet{margin-top:96px}.u-mr96--tablet{margin-right:96px}.u-mb96--tablet{margin-bottom:96px}.u-mt120--tablet{margin-top:120px}.u-mr120--tablet{margin-right:120px}.u-mb120--tablet{margin-bottom:120px}.u-mt128--tablet{margin-top:128px}.u-mr128--tablet{margin-right:128px}.u-mb128--tablet{margin-bottom:128px}.u-mt148--tablet{margin-top:148px}.u-mr148--tablet{margin-right:148px}.u-mb148--tablet{margin-bottom:148px}}@media (max-width:1224px){.u-mt0--desktop-s{margin-top:0}.u-mr0--desktop-s{margin-right:0}.u-mb0--desktop-s{margin-bottom:0}.u-mt4--desktop-s{margin-top:4px}.u-mr4--desktop-s{margin-right:4px}.u-mb4--desktop-s{margin-bottom:4px}.u-mt6--desktop-s{margin-top:6px}.u-mr6--desktop-s{margin-right:6px}.u-mb6--desktop-s{margin-bottom:6px}.u-mt8--desktop-s{margin-top:8px}.u-mr8--desktop-s{margin-right:8px}.u-mb8--desktop-s{margin-bottom:8px}.u-mt10--desktop-s{margin-top:10px}.u-mr10--desktop-s{margin-right:10px}.u-mb10--desktop-s{margin-bottom:10px}.u-mt12--desktop-s{margin-top:12px}.u-mr12--desktop-s{margin-right:12px}.u-mb12--desktop-s{margin-bottom:12px}.u-mt16--desktop-s{margin-top:16px}.u-mr16--desktop-s{margin-right:16px}.u-mb16--desktop-s{margin-bottom:16px}.u-mt20--desktop-s{margin-top:20px}.u-mr20--desktop-s{margin-right:20px}.u-mb20--desktop-s{margin-bottom:20px}.u-mt24--desktop-s{margin-top:24px}.u-mr24--desktop-s{margin-right:24px}.u-mb24--desktop-s{margin-bottom:24px}.u-mt28--desktop-s{margin-top:28px}.u-mr28--desktop-s{margin-right:28px}.u-mb28--desktop-s{margin-bottom:28px}.u-mt32--desktop-s{margin-top:32px}.u-mr32--desktop-s{margin-right:32px}.u-mb32--desktop-s{margin-bottom:32px}.u-mt36--desktop-s{margin-top:36px}.u-mr36--desktop-s{margin-right:36px}.u-mb36--desktop-s{margin-bottom:36px}.u-mt40--desktop-s{margin-top:40px}.u-mr40--desktop-s{margin-right:40px}.u-mb40--desktop-s{margin-bottom:40px}.u-mt48--desktop-s{margin-top:48px}.u-mr48--desktop-s{margin-right:48px}.u-mb48--desktop-s{margin-bottom:48px}.u-mt56--desktop-s{margin-top:56px}.u-mr56--desktop-s{margin-right:56px}.u-mb56--desktop-s{margin-bottom:56px}.u-mt64--desktop-s{margin-top:64px}.u-mr64--desktop-s{margin-right:64px}.u-mb64--desktop-s{margin-bottom:64px}.u-mt72--desktop-s{margin-top:72px}.u-mr72--desktop-s{margin-right:72px}.u-mb72--desktop-s{margin-bottom:72px}.u-mt80--desktop-s{margin-top:80px}.u-mr80--desktop-s{margin-right:80px}.u-mb80--desktop-s{margin-bottom:80px}.u-mt96--desktop-s{margin-top:96px}.u-mr96--desktop-s{margin-right:96px}.u-mb96--desktop-s{margin-bottom:96px}.u-mt120--desktop-s{margin-top:120px}.u-mr120--desktop-s{margin-right:120px}.u-mb120--desktop-s{margin-bottom:120px}.u-mt128--desktop-s{margin-top:128px}.u-mr128--desktop-s{margin-right:128px}.u-mb128--desktop-s{margin-bottom:128px}.u-mt148--desktop-s{margin-top:148px}.u-mr148--desktop-s{margin-right:148px}.u-mb148--desktop-s{margin-bottom:148px}}@media (min-width:1632px){.u-mt0--desktop-l{margin-top:0}.u-mr0--desktop-l{margin-right:0}.u-mb0--desktop-l{margin-bottom:0}.u-mt4--desktop-l{margin-top:4px}.u-mr4--desktop-l{margin-right:4px}.u-mb4--desktop-l{margin-bottom:4px}.u-mt6--desktop-l{margin-top:6px}.u-mr6--desktop-l{margin-right:6px}.u-mb6--desktop-l{margin-bottom:6px}.u-mt8--desktop-l{margin-top:8px}.u-mr8--desktop-l{margin-right:8px}.u-mb8--desktop-l{margin-bottom:8px}.u-mt10--desktop-l{margin-top:10px}.u-mr10--desktop-l{margin-right:10px}.u-mb10--desktop-l{margin-bottom:10px}.u-mt12--desktop-l{margin-top:12px}.u-mr12--desktop-l{margin-right:12px}.u-mb12--desktop-l{margin-bottom:12px}.u-mt16--desktop-l{margin-top:16px}.u-mr16--desktop-l{margin-right:16px}.u-mb16--desktop-l{margin-bottom:16px}.u-mt20--desktop-l{margin-top:20px}.u-mr20--desktop-l{margin-right:20px}.u-mb20--desktop-l{margin-bottom:20px}.u-mt24--desktop-l{margin-top:24px}.u-mr24--desktop-l{margin-right:24px}.u-mb24--desktop-l{margin-bottom:24px}.u-mt28--desktop-l{margin-top:28px}.u-mr28--desktop-l{margin-right:28px}.u-mb28--desktop-l{margin-bottom:28px}.u-mt32--desktop-l{margin-top:32px}.u-mr32--desktop-l{margin-right:32px}.u-mb32--desktop-l{margin-bottom:32px}.u-mt36--desktop-l{margin-top:36px}.u-mr36--desktop-l{margin-right:36px}.u-mb36--desktop-l{margin-bottom:36px}.u-mt40--desktop-l{margin-top:40px}.u-mr40--desktop-l{margin-right:40px}.u-mb40--desktop-l{margin-bottom:40px}.u-mt48--desktop-l{margin-top:48px}.u-mr48--desktop-l{margin-right:48px}.u-mb48--desktop-l{margin-bottom:48px}.u-mt56--desktop-l{margin-top:56px}.u-mr56--desktop-l{margin-right:56px}.u-mb56--desktop-l{margin-bottom:56px}.u-mt64--desktop-l{margin-top:64px}.u-mr64--desktop-l{margin-right:64px}.u-mb64--desktop-l{margin-bottom:64px}.u-mt72--desktop-l{margin-top:72px}.u-mr72--desktop-l{margin-right:72px}.u-mb72--desktop-l{margin-bottom:72px}.u-mt80--desktop-l{margin-top:80px}.u-mr80--desktop-l{margin-right:80px}.u-mb80--desktop-l{margin-bottom:80px}.u-mt96--desktop-l{margin-top:96px}.u-mr96--desktop-l{margin-right:96px}.u-mb96--desktop-l{margin-bottom:96px}.u-mt120--desktop-l{margin-top:120px}.u-mr120--desktop-l{margin-right:120px}.u-mb120--desktop-l{margin-bottom:120px}.u-mt128--desktop-l{margin-top:128px}.u-mr128--desktop-l{margin-right:128px}.u-mb128--desktop-l{margin-bottom:128px}.u-mt148--desktop-l{margin-top:148px}.u-mr148--desktop-l{margin-right:148px}.u-mb148--desktop-l{margin-bottom:148px}}.u-flex{display:flex}.mobile{display:none}@media (max-width:600px){.mobile{display:block}}.tablet{display:none}@media (max-width:800px){.tablet{display:block}}.desktop-s{display:block}@media (max-width:800px){.desktop-s{display:none}}.desktop{display:block}@media (max-width:1224px){.desktop{display:none}}.desktop-l{display:block}@media (max-width:1632px){.desktop-l{display:none}}.mobile--flex{display:none}@media (max-width:600px){.mobile--flex{display:flex}}.tablet--flex{display:none}@media (max-width:800px){.tablet--flex{display:flex}}.desktop-s--flex{display:flex}@media (max-width:800px){.desktop-s--flex{display:none}}.desktop--flex{display:flex}@media (max-width:1224px){.desktop--flex{display:none}}.u-flex-column{flex-direction:column}.u-align-center{align-items:center}.u-justify-center{justify-content:center}.u-justify-space{justify-content:space-between}.u-default{cursor:default}@font-face{font-family:UberMove;src:url(https://d1a3f4spazzrp4.cloudfront.net/dotcom-assets/fonts/UberMove-Regular.woff2) format("woff2"),url(https://d1a3f4spazzrp4.cloudfront.net/dotcom-assets/fonts/UberMove-Regular.woff) format("woff");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:UberMove;src:url(https://d1a3f4spazzrp4.cloudfront.net/dotcom-assets/fonts/UberMove-Medium.woff2) format("woff2"),url(https://d1a3f4spazzrp4.cloudfront.net/dotcom-assets/fonts/UberMove-Medium.woff) format("woff");font-weight:500;font-style:normal;font-display:swap}@font-face{font-family:UberMove;src:url(https://d1a3f4spazzrp4.cloudfront.net/dotcom-assets/fonts/UberMove-Bold.woff2) format("woff2"),url(https://d1a3f4spazzrp4.cloudfront.net/dotcom-assets/fonts/UberMove-Bold.woff) format("woff");font-weight:600;font-style:normal;font-display:swap}body{font-family:UberMove,Helvetica,Arial,sans-serif}a{color:#0075C9;font-weight:600}.container-gl{position:relative;top:-330px}.top-paragraphs{padding-top:150px;display:flex}@media (max-width:800px){.top-paragraphs{padding-top:80px}}.logo-deckgl{position:relative;top:-490px}.logo-deckgl .logo-summit{width:450px}.openjs-logo{width:200px}.gradient-border{--border-width:1px;position:relative;display:flex;justify-content:center;background:#fff}.gradient-border::after{position:absolute;content:"";top:calc(-1 * var(--border-width));left:calc(-1 * var(--border-width));z-index:-1;width:calc(100% + var(--border-width) * 2);height:calc(100% + var(--border-width) * 2);background:linear-gradient(90deg,#06ffef,#fff,#066788,#0c2960,#00ade6);background-size:300% 300%;background-position:0 50%;animation:moveGradient 5s alternate infinite}.gradient-border.vertical{position:absolute;z-index:-10;width:1px;height:2800px}.gradient-border.vertical::after{width:calc(100% + var(--border-width) * .5)}@media (max-width:800px){.gradient-border.vertical{display:none}}@keyframes moveGradient{50%{background-position:100% 50%}}.paragraph-1{padding:0 40px 60px 0}@media (max-width:800px){.paragraph-1{padding:unset}}.paragraph-2{padding:0 0 60px 50px}@media (max-width:800px){.paragraph-2{padding:unset}}.paragraph-3{padding:90px 70px 50px;text-align:center}@media (max-width:800px){.paragraph-3{padding:50px 30px 40px}}.paragraph-4{padding-right:60px}@media (max-width:800px){.paragraph-4{padding:unset}}.paragraph-5{padding-left:60px}@media (max-width:800px){.paragraph-5{padding:unset}}.button-gl{font-size:15px;line-height:44px;letter-spacing:2px;font-weight:700;padding:0 3rem;pointer-events:all;display:inline-block;text-decoration:none!important;cursor:pointer;transition:background-color 250ms ease-in 0s,color 250ms ease-in 0s;border-width:2px;border-style:solid;border-color:#00ade6;color:#2b3848;border-image:linear-gradient(to right,#0c2960 0,#00ade6 100%) 2/1/0 stretch}.button-gl:hover{color:#fff;background-color:#00ade6}.title-gl{font-family:UberMove,Helvetica,Arial,sans-serif;font-weight:500}.footer-gl{height:90px;width:100%;background-image:url(https://raw.githubusercontent.com/visgl/deck.gl-data/master/events/madrid-summit-2022/footer-bg.png)}#agenda{padding:90px 80px}@media (min-width:1632px){#agenda{padding:120px 100px}}#agenda .card-agenda{padding:0 40px 0 0}@media (max-width:800px){#agenda .card-agenda{padding:0 20px 0 0}}@media (max-width:600px){#agenda .card-agenda{padding:0}}@media (min-width:1632px){#agenda .card-agenda{padding:0 80px 0 0}}#agenda .card-agenda .card-bg{background-color:#f4f4f4;padding:30px 50px;height:450px}@media (min-width:1632px){#agenda .card-agenda .card-bg{padding:50px 40px;height:500px}}#agenda .card-agenda .card-bg .author{max-width:85px;max-height:85px;}#agenda .card-agenda .card-bg .author-title{display:block;}#agenda .divider-agenda{width:100%;background-color:#f4f4f4;font-weight:500}#agenda .divider-agenda p{padding:10px 30px}header{height:628px;background-color:#202020;}.header{background-size:cover;background-position:center;background-repeat:no-repeat;display:flex;flex-direction:column;color:#fff;justify-content:center;}.header .container{display:flex;align-items:start;}.header img{object-fit:contain;width:360px;max-width:25%;margin:1em 5% 5% 0;}h1{font-size:4em;line-height:1.2;}h1 b{font-weight:700;}@media (max-width:800px) {.header .container{flex-direction: column;padding-left:10%;}.header img{width:260px;max-width: 50%;}} \ No newline at end of file