diff --git a/examples/website/360-video/app.jsx b/examples/website/360-video/app.tsx similarity index 89% rename from examples/website/360-video/app.jsx rename to examples/website/360-video/app.tsx index e608b5edb05..dc745e9d2f2 100644 --- a/examples/website/360-video/app.jsx +++ b/examples/website/360-video/app.tsx @@ -1,11 +1,12 @@ import React, {useEffect, useState} from 'react'; import {createRoot} from 'react-dom/client'; - import DeckGL from '@deck.gl/react'; import {FirstPersonView, COORDINATE_SYSTEM} from '@deck.gl/core'; import {SimpleMeshLayer} from '@deck.gl/mesh-layers'; import {SphereGeometry} from '@luma.gl/engine'; +import type {FirstPersonViewState} from '@deck.gl/core'; + // Video created by the NASA Jet Propulsion Laboratory, Public domain, via Wikimedia Commons // Source: https://commons.wikimedia.org/wiki/File:NASA_VR-360_Astronaut_Training-_Space_Walk.webm const VIDEO_URL = @@ -17,7 +18,7 @@ const sphere = new SphereGeometry({ radius: 150 }); -const PLAY_BUTTON_STYLE = { +const PLAY_BUTTON_STYLE: React.CSSProperties = { width: '100%', height: '100%', display: 'flex', @@ -26,7 +27,7 @@ const PLAY_BUTTON_STYLE = { opacity: 0.5 }; -const INITIAL_VIEW_STATE = { +const INITIAL_VIEW_STATE: FirstPersonViewState = { latitude: 0, longitude: 0, position: [0, 0, 0], @@ -36,7 +37,7 @@ const INITIAL_VIEW_STATE = { export default function App() { const [isPlaying, setPlaying] = useState(false); - const [video, setVideo] = useState(null); + const [video, setVideo] = useState(); useEffect(() => { let videoEl; @@ -95,6 +96,6 @@ export default function App() { ); } -export function renderToDOM(container) { +export function renderToDOM(container: HTMLDivElement) { createRoot(container).render(); } diff --git a/examples/website/360-video/index.html b/examples/website/360-video/index.html index 72ae831cd54..6d3dfc18a0f 100644 --- a/examples/website/360-video/index.html +++ b/examples/website/360-video/index.html @@ -12,7 +12,7 @@
diff --git a/examples/website/360-video/package.json b/examples/website/360-video/package.json index 6f9d41021ea..26087beba51 100644 --- a/examples/website/360-video/package.json +++ b/examples/website/360-video/package.json @@ -9,6 +9,8 @@ "build": "vite build" }, "dependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "deck.gl": "^9.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/examples/website/360-video/tsconfig.json b/examples/website/360-video/tsconfig.json new file mode 100644 index 00000000000..9b3c020493c --- /dev/null +++ b/examples/website/360-video/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/examples/website/collision-filter/app.jsx b/examples/website/collision-filter/app.jsx deleted file mode 100644 index 1f70a31688e..00000000000 --- a/examples/website/collision-filter/app.jsx +++ /dev/null @@ -1,86 +0,0 @@ -/* global fetch */ -import React, {useEffect, useMemo, useState} from 'react'; -import {createRoot} from 'react-dom/client'; -import {Map} from 'react-map-gl//maplibre'; -import DeckGL from '@deck.gl/react'; -import {GeoJsonLayer, TextLayer} from '@deck.gl/layers'; -import {CollisionFilterExtension} from '@deck.gl/extensions'; -import calculateLabels from './calculateLabels'; - -const DATA_URL = - 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/collision-filter/ne_10_roads_mexico.json'; -const LINE_COLOR = [0, 173, 230]; - -const initialViewState = {longitude: -100, latitude: 24, zoom: 5, minZoom: 5, maxZoom: 12}; - -export default function App({ - url = DATA_URL, - mapStyle = 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json', - sizeScale = 10, - collisionEnabled = true, - pointSpacing = 5 -}) { - const [roads, setRoads] = useState({type: 'FeatureCollection', features: []}); - useEffect(() => { - fetch(url) - .then(r => r.json()) - .then(data => setRoads(data)); - }, [url]); - - const dataLabels = useMemo(() => calculateLabels(roads, pointSpacing), [roads, pointSpacing]); - - const layers = [ - new GeoJsonLayer({ - id: 'roads-outline', - data: roads, - lineWidthMinPixels: 4, - parameters: {depthTest: false}, - getLineColor: [255, 255, 255] - }), - new GeoJsonLayer({ - id: 'roads', - data: roads, - lineWidthMinPixels: 2.5, - parameters: {depthTest: false}, - getLineColor: LINE_COLOR - }), - new TextLayer({ - id: 'text-layer', - data: dataLabels, - getColor: [0, 0, 0], - getBackgroundColor: LINE_COLOR, - getBorderColor: [10, 16, 29], - getBorderWidth: 2, - getSize: 18, - billboard: false, - getAngle: d => d.angle, - getTextAnchor: 'middle', - getAlignmentBaseline: 'center', - background: true, - backgroundPadding: [4, 1], - outlineWidth: 0, - outlineColor: [255, 255, 0], - fontSettings: { - sdf: true - }, - characterSet: '0123456789ABCD', - fontFamily: 'monospace', - - // CollisionFilterExtension props - collisionEnabled, - getCollisionPriority: d => d.priority, - collisionTestProps: {sizeScale}, - extensions: [new CollisionFilterExtension()] - }) - ]; - - return ( - - - - ); -} - -export function renderToDOM(container) { - createRoot(container).render(); -} diff --git a/examples/website/collision-filter/app.tsx b/examples/website/collision-filter/app.tsx new file mode 100644 index 00000000000..571bdf6477b --- /dev/null +++ b/examples/website/collision-filter/app.tsx @@ -0,0 +1,102 @@ +/* global fetch */ +import React, {useEffect, useMemo, useState} from 'react'; +import {createRoot} from 'react-dom/client'; +import {Map} from 'react-map-gl//maplibre'; +import DeckGL from '@deck.gl/react'; +import {GeoJsonLayer, TextLayer} from '@deck.gl/layers'; +import {CollisionFilterExtension, CollisionFilterExtensionProps} from '@deck.gl/extensions'; +import {calculateLabels, Label} from './calculate-labels'; + +import type {Position, MapViewState} from '@deck.gl/core'; +import type {FeatureCollection, Geometry} from 'geojson'; + +const DATA_URL = + 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/collision-filter/ne_10_roads.json'; + +const INITIAL_VIEW_STATE: MapViewState = { + longitude: -100, + latitude: 24, + zoom: 5, + minZoom: 5, + maxZoom: 12 +}; + +type RoadProperties = { + name: string; + scalerank: number; +}; + +export default function App({ + mapStyle = 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json', + sizeScale = 10, + collisionEnabled = true, + pointSpacing = 5 +}: { + mapStyle?: string; + sizeScale?: number; + collisionEnabled?: boolean; + pointSpacing?: number +}) { + const [roads, setRoads] = useState>(); + + useEffect(() => { + fetch(DATA_URL) + .then(resp => resp.json()) + .then(setRoads); + }, []); + + const roadLabels = useMemo(() => calculateLabels(roads, d => d.name !== null, pointSpacing), [roads, pointSpacing]); + + const layers = [ + new GeoJsonLayer({ + id: 'roads-outline', + data: roads, + getLineWidth: f => f.properties.scalerank + 2, + lineWidthScale: 40, + lineWidthMinPixels: 3, + getLineColor: [0, 173, 230] + }), + new GeoJsonLayer({ + id: 'roads', + data: roads, + getLineWidth: f => f.properties.scalerank, + lineWidthScale: 40, + lineWidthMinPixels: 1, + parameters: {depthCompare: 'always'}, + getLineColor: [255, 255, 255] + }), + new TextLayer, CollisionFilterExtensionProps>({ + id: 'labels', + data: roadLabels, + getColor: [0, 0, 0], + getBackgroundColor: [255, 255, 255], + getBorderColor: [0, 173, 230], + getBorderWidth: 1, + getPosition: d => d.position as Position, + getText: d => d.parent.name, + getSize: d => Math.max(d.parent.scalerank * 2, 10), + getAngle: d => d.angle, + billboard: false, + background: true, + backgroundPadding: [4, 1], + characterSet: 'auto', + fontFamily: 'monospace', + + // CollisionFilterExtension props + collisionEnabled, + getCollisionPriority: d => d.priority, + collisionTestProps: {sizeScale}, + extensions: [new CollisionFilterExtension()] + }) + ]; + + return ( + + + + ); +} + +export function renderToDOM(container: HTMLDivElement) { + createRoot(container).render(); +} diff --git a/examples/website/collision-filter/calculate-labels.ts b/examples/website/collision-filter/calculate-labels.ts new file mode 100644 index 00000000000..3019f43cb54 --- /dev/null +++ b/examples/website/collision-filter/calculate-labels.ts @@ -0,0 +1,83 @@ +import {geomEach, along, rhumbBearing, lineDistance, lineString, booleanEqual} from '@turf/turf'; +import type {FeatureCollection, Geometry, Feature, LineString} from 'geojson'; + +export type Label = { + position: number[]; + priority: number; + angle: number; + parent: FeatureProperties; +}; + +/* Utility to add rotated labels along lines and assign collision priorities */ +export function calculateLabels( + geojson: FeatureCollection | undefined, + filter: (properties: FeatureProperties) => boolean, + pointSpacing: number +): Label[] { + if (!geojson) return []; + + const result: Label[] = []; + + function addLabelsAlongLineString(coordinates: LineString["coordinates"], properties: FeatureProperties) { + // Add labels to minimize overlaps, pick odd values from each level + // 1 <- depth 1 + // 1 2 3 <- depth 2 + // 1 2 3 4 5 6 7 <- depth 3 + const feature = lineString(coordinates, properties); + const lineLength = Math.floor(lineDistance(feature)); + let delta = lineLength / 2; // Spacing between points at level + let depth = 1; + while (delta > pointSpacing) { + for (let i = 1; i < 2 ** depth; i += 2) { + const label = getLabelAtPoint(feature, lineLength, i * delta, 100 - depth); // Top levels have highest priority + result.push(label); + } + depth++; + delta /= 2; + } + } + + // @ts-ignore turf type FeatureCollection is not compatible with geojson type + geomEach(geojson, (geometry: Geometry, i, properties: FeatureProperties) => { + if (!filter(properties)) return; + + switch (geometry.type) { + case 'LineString': + addLabelsAlongLineString(geometry.coordinates, properties); + break; + + case 'MultiLineString': + for (const coordinates of geometry.coordinates) { + addLabelsAlongLineString(coordinates, properties); + } + break; + + default: + // ignore + } + }); + + return result; +} + +function getLabelAtPoint( + line: Feature, + lineLength: number, + dAlong: number, + priority: number +): Label { + const offset = dAlong + 1 < lineLength ? 1 : -1; + const point = along(line, dAlong); + const nextPoint = along(line, dAlong + offset); + if (booleanEqual(point, nextPoint)) return; + + let angle = 90 - rhumbBearing(point, nextPoint); + if (Math.abs(angle) > 90) angle += 180; + + return { + position: point.geometry.coordinates, + priority, + angle, + parent: line.properties + }; +} diff --git a/examples/website/collision-filter/calculateLabels.js b/examples/website/collision-filter/calculateLabels.js deleted file mode 100644 index 20a94cb5640..00000000000 --- a/examples/website/collision-filter/calculateLabels.js +++ /dev/null @@ -1,48 +0,0 @@ -import * as turf from '@turf/turf'; - -/* Utility to add rotated labels along lines and assign collision priorities */ -export default function calculateLabels(data, pointSpacing) { - const routes = data.features.filter(d => d.geometry.type !== 'Point'); - const result = []; - - function addPoint(lineLength, lineString, dAlong, name, priority) { - let offset = 1; - if (dAlong > 0.5 * lineLength) offset *= -1; - const feature = turf.along(lineString, dAlong); - const nextFeature = turf.along(lineString, dAlong + offset); - const {coordinates} = feature.geometry; - const next = nextFeature.geometry.coordinates; - if (coordinates[0] === next[0] && coordinates[1] === next[1]) return; - - let angle = 90 - turf.rhumbBearing(coordinates, next); - if (Math.abs(angle) > 90) angle += 180; - - result.push({position: coordinates, text: name, priority, angle}); - } - - // Add points along the lines - for (const feature of routes) { - const lineLength = Math.floor(turf.lineDistance(feature.geometry)); - const {name} = feature.properties; - - feature.geometry.coordinates.forEach(c => { - const lineString = turf.lineString(c); - - // Add labels to minimize overlaps, pick odd values from each level - // 1 <- depth 1 - // 1 2 3 <- depth 2 - // 1 2 3 4 5 6 7 <- depth 3 - let delta = 0.5 * lineLength; // Spacing between points at level - let depth = 1; - while (delta > pointSpacing) { - for (let i = 1; i < 2 ** depth; i += 2) { - addPoint(lineLength, lineString, i * delta, name, 100 - depth); // Top levels have highest priority - } - depth++; - delta /= 2; - } - }); - } - - return result; -} diff --git a/examples/website/collision-filter/index.html b/examples/website/collision-filter/index.html index 21e34f8ec04..d91d4471749 100644 --- a/examples/website/collision-filter/index.html +++ b/examples/website/collision-filter/index.html @@ -12,7 +12,7 @@
diff --git a/examples/website/collision-filter/package.json b/examples/website/collision-filter/package.json index cbb270f5dc8..5e6d7dbf700 100644 --- a/examples/website/collision-filter/package.json +++ b/examples/website/collision-filter/package.json @@ -9,12 +9,14 @@ "build": "vite build" }, "dependencies": { + "@turf/turf": "6.5.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "deck.gl": "^9.0.0", "react": "^18.0.0", "react-dom": "^18.0.0", "maplibre-gl": "^3.0.0", - "react-map-gl": "^7.1.0", - "@turf/turf": "6.5.0" + "react-map-gl": "^7.1.0" }, "devDependencies": { "typescript": "^4.6.0", diff --git a/examples/website/collision-filter/tsconfig.json b/examples/website/collision-filter/tsconfig.json new file mode 100644 index 00000000000..9b3c020493c --- /dev/null +++ b/examples/website/collision-filter/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/examples/website/election/README.md b/examples/website/election/README.md deleted file mode 100644 index 26545994a5b..00000000000 --- a/examples/website/election/README.md +++ /dev/null @@ -1,28 +0,0 @@ -This is a minimal standalone version of the Google Maps Integration example -on [deck.gl](http://deck.gl) website. - -Note that this example demonstrates using deck.gl with Google Maps. For other base map options, visit the project templates in [get-started](/examples/get-started). - - -### Usage - -To run this example, you need a [Google Maps API key](https://developers.google.com/maps/documentation/javascript/get-api-key). You can either set an environment variable: - -```bash -export GoogleMapsAPIKey= -``` - -Or set the `GOOGLE_MAPS_API_KEY` variable in `app.js`. - -```bash -# install dependencies -npm install -# or -yarn -# bundle and serve the app with vite -npm start -``` - -### Data Source - -To build your own application with deck.gl and Google Maps, check out the [documentation of @deck.gl/google-maps module](../../../docs/api-reference/google-maps/overview.md) diff --git a/examples/website/election/app.js b/examples/website/election/app.js deleted file mode 100644 index b9b3f254e13..00000000000 --- a/examples/website/election/app.js +++ /dev/null @@ -1,140 +0,0 @@ -/* global document, google */ -import {GoogleMapsOverlay as DeckOverlay} from '@deck.gl/google-maps'; -import {ScatterplotLayer} from '@deck.gl/layers'; -import {scaleLog} from 'd3-scale'; - -import mapStyle from './map-style'; - -const DATA_URL = - 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/election/votes-by-county.json'; - -// Set your Google Maps API key here or via environment variable -const GOOGLE_MAPS_API_KEY = process.env.GoogleMapsAPIKey; // eslint-disable-line - -function loadScript(url) { - if (typeof google !== 'undefined') { - return Promise.resolve(); - } - return new Promise(resolve => { - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = url; - script.onload = resolve; - document.head.appendChild(script); - }); -} - -// Map delta percentage points to color -const repColorScale = scaleLog() - .domain([1, 100]) - .range([ - [255, 255, 191], - [215, 25, 28] - ]); -const demColorScale = scaleLog() - .domain([1, 100]) - .range([ - [255, 255, 191], - [43, 131, 186] - ]); - -export async function renderToDOM(container, options = {}) { - await loadScript( - `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=visualization&v=3.42` - ); - - const map = new google.maps.Map(container, { - center: {lat: 40, lng: -100}, - zoom: 5, - styles: mapStyle, - mapTypeId: 'terrain', - mapTypeControlOptions: { - mapTypeIds: ['roadmap', 'terrain'] - }, - streetViewControl: false - }); - - const overlay = new DeckOverlay({ - parameters: { - // Additive blending - blendColorSrcFactor: 'src-alpha', - blendColorDstFactor: 'one', - blendAlphaSrcFactor: 'one', - blendAlphaDstFactor: 'one-minus-dst-alpha' - }, - layers: renderLayers(options), - getTooltip - }); - - overlay.setMap(map); - - return { - update: newOptions => { - overlay.setProps({ - layers: renderLayers(newOptions) - }); - }, - remove: () => { - overlay.finalize(); - } - }; -} - -function getTooltip({object, layer}) { - if (!object) { - return null; - } - const {year} = layer.props; - const votes = object[year]; - return { - html: `\ -

${object.name}

-
- Total: ${votes.total} -
Democrat: ${((votes.dem / votes.total) * 100).toFixed(1)}% -
Republican: ${((votes.rep / votes.total) * 100).toFixed(1)}% -
` - }; -} - -function renderLayers({data = DATA_URL, year = 2016}) { - return [ - new ScatterplotLayer({ - data, - opacity: 0.7, - getPosition: d => [d.longitude, d.latitude], - getRadius: d => { - const votes = d[year]; - return votes ? Math.sqrt(votes.total) : 0; - }, - getFillColor: d => { - const votes = d[year]; - const demPercent = (votes.dem / votes.total) * 100; - const repPercent = (votes.rep / votes.total) * 100; - return demPercent >= repPercent - ? demColorScale(demPercent - repPercent + 1) - : repColorScale(repPercent - demPercent + 1); - }, - radiusUnits: 'pixels', - radiusScale: 0.02, - radiusMinPixels: 2, - - pickable: true, - autoHighlight: true, - highlightColor: [255, 200, 0, 200], - - updateTriggers: { - getRadius: year, - getFillColor: year - }, - transitions: { - getRadius: 1000, - getFillColor: 1000 - }, - - // This is not a standard ScatterplotLayer prop - // We attach it to the layer to access it in getTooltip - year - }) - ]; -} diff --git a/examples/website/election/index.html b/examples/website/election/index.html deleted file mode 100644 index 95f0b8c49de..00000000000 --- a/examples/website/election/index.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - google-maps-example - - - -
- - - diff --git a/examples/website/election/map-style.js b/examples/website/election/map-style.js deleted file mode 100644 index 7904ff0dc72..00000000000 --- a/examples/website/election/map-style.js +++ /dev/null @@ -1,169 +0,0 @@ -// https://snazzymaps.com/style/38/shades-of-grey -// by Adam Krogh -export default [ - { - featureType: 'all', - elementType: 'labels.text.fill', - stylers: [ - { - saturation: 36 - }, - { - color: '#000000' - }, - { - lightness: 40 - } - ] - }, - { - featureType: 'all', - elementType: 'labels.text.stroke', - stylers: [ - { - visibility: 'on' - }, - { - color: '#000000' - }, - { - lightness: 16 - } - ] - }, - { - featureType: 'all', - elementType: 'labels.icon', - stylers: [ - { - visibility: 'off' - } - ] - }, - { - featureType: 'administrative', - elementType: 'geometry.fill', - stylers: [ - { - color: '#000000' - }, - { - lightness: 20 - } - ] - }, - { - featureType: 'administrative', - elementType: 'geometry.stroke', - stylers: [ - { - color: '#000000' - }, - { - lightness: 17 - }, - { - weight: 1.2 - } - ] - }, - { - featureType: 'landscape', - elementType: 'geometry', - stylers: [ - { - color: '#000000' - }, - { - lightness: 20 - } - ] - }, - { - featureType: 'poi', - elementType: 'geometry', - stylers: [ - { - color: '#000000' - }, - { - lightness: 21 - } - ] - }, - { - featureType: 'road.highway', - elementType: 'geometry.fill', - stylers: [ - { - color: '#000000' - }, - { - lightness: 17 - } - ] - }, - { - featureType: 'road.highway', - elementType: 'geometry.stroke', - stylers: [ - { - color: '#000000' - }, - { - lightness: 29 - }, - { - weight: 0.2 - } - ] - }, - { - featureType: 'road.arterial', - elementType: 'geometry', - stylers: [ - { - color: '#000000' - }, - { - lightness: 18 - } - ] - }, - { - featureType: 'road.local', - elementType: 'geometry', - stylers: [ - { - color: '#000000' - }, - { - lightness: 16 - } - ] - }, - { - featureType: 'transit', - elementType: 'geometry', - stylers: [ - { - color: '#000000' - }, - { - lightness: 19 - } - ] - }, - { - featureType: 'water', - elementType: 'geometry', - stylers: [ - { - color: '#000000' - }, - { - lightness: 17 - } - ] - } -]; diff --git a/examples/website/election/package.json b/examples/website/election/package.json deleted file mode 100644 index b8c2768fc84..00000000000 --- a/examples/website/election/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "deckgl-examples-election", - "version": "0.0.0", - "private": true, - "license": "MIT", - "scripts": { - "start": "vite --open", - "start-local": "vite --config ../../vite.config.local.mjs", - "build": "vite build" - }, - "dependencies": { - "d3-scale": "^4.0.0", - "deck.gl": "^9.0.0" - }, - "devDependencies": { - "typescript": "^4.6.0", - "vite": "^4.0.0" - } -} diff --git a/examples/website/election/vite.config.js b/examples/website/election/vite.config.js deleted file mode 100644 index c782ed83e21..00000000000 --- a/examples/website/election/vite.config.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - define: { - 'process.env.GoogleMapsAPIKey': JSON.stringify(process.env.GoogleMapsAPIKey) - } -}; diff --git a/examples/website/globe/animated-arc-group-layer.js b/examples/website/globe/animated-arc-group-layer.ts similarity index 61% rename from examples/website/globe/animated-arc-group-layer.js rename to examples/website/globe/animated-arc-group-layer.ts index 65b095f29cc..e272752d815 100644 --- a/examples/website/globe/animated-arc-group-layer.js +++ b/examples/website/globe/animated-arc-group-layer.ts @@ -1,18 +1,32 @@ -import {CompositeLayer} from '@deck.gl/core'; -import AnimatedArcLayer from './animated-arc-layer'; +import {CompositeLayer, UpdateParameters} from '@deck.gl/core'; +import AnimatedArcLayer, {AnimatedArcLayerProps} from './animated-arc-layer'; const MAX_ARCS_PER_LAYER = 2500; +type ArcsGroup = { + startTime: number; + endTime: number; + data: DataT[]; +}; + /** Same effect as the AnimatedArcLayer, but perf optimized. * Data is divided into smaller groups, and one sub layer is rendered for each group. * This allows us to cheaply cull invisible arcs by turning layers off and on. */ -export default class AnimatedArcGroupLayer extends CompositeLayer { - updateState({props, changeFlags}) { +export default class AnimatedArcGroupLayer extends CompositeLayer>> { + layerName = 'AnimatedArcGroupLayer'; + defaultProps = AnimatedArcLayer.defaultProps; + + state!: { + groups: ArcsGroup[]; + }; + + updateState({props, changeFlags}: UpdateParameters) { if (changeFlags.dataChanged) { // Sort and group data const {data, getSourceTimestamp, getTargetTimestamp} = props; - const groups = sortAndGroup(data, getSourceTimestamp, getTargetTimestamp, MAX_ARCS_PER_LAYER); + // @ts-ignore + const groups = sortAndGroup(data, getSourceTimestamp, getTargetTimestamp); this.setState({groups}); } } @@ -36,12 +50,14 @@ export default class AnimatedArcGroupLayer extends CompositeLayer { } } -AnimatedArcGroupLayer.layerName = 'AnimatedArcGroupLayer'; -AnimatedArcGroupLayer.defaultProps = AnimatedArcLayer.defaultProps; - -function sortAndGroup(data, getStartTime, getEndTime, groupSize) { - const groups = []; - let group = null; +function sortAndGroup( + data: DataT[], + getStartTime: (d: DataT) => number, + getEndTime: (d: DataT) => number, + groupSize: number = MAX_ARCS_PER_LAYER +): ArcsGroup[] { + const groups: ArcsGroup[] = []; + let group: ArcsGroup; data.sort((d1, d2) => getStartTime(d1) - getStartTime(d2)); diff --git a/examples/website/globe/animated-arc-layer.js b/examples/website/globe/animated-arc-layer.ts similarity index 62% rename from examples/website/globe/animated-arc-layer.js rename to examples/website/globe/animated-arc-layer.ts index 7b2b334f263..9765ae88e97 100644 --- a/examples/website/globe/animated-arc-layer.js +++ b/examples/website/globe/animated-arc-layer.ts @@ -1,6 +1,24 @@ -import {ArcLayer} from '@deck.gl/layers'; +import {ArcLayer, ArcLayerProps} from '@deck.gl/layers'; +import {Accessor, DefaultProps} from '@deck.gl/core'; + +export type AnimatedArcLayerProps = _AnimatedArcLayerProps & ArcLayerProps; + +type _AnimatedArcLayerProps = { + getSourceTimestamp?: Accessor; + getTargetTimestamp?: Accessor; + timeRange?: [number, number]; +}; + +const defaultProps: DefaultProps<_AnimatedArcLayerProps> = { + getSourceTimestamp: {type: 'accessor', value: 0}, + getTargetTimestamp: {type: 'accessor', value: 1}, + timeRange: {type: 'array', compare: true, value: [0, 1]} +}; + +export default class AnimatedArcLayer extends ArcLayer> { + layerName = 'AnimatedArcLayer'; + defaultProps = defaultProps; -export default class AnimatedArcLayer extends ArcLayer { getShaders() { const shaders = super.getShaders(); shaders.inject = { @@ -44,16 +62,10 @@ color.a *= (vTimestamp - timeRange.x) / (timeRange.y - timeRange.x); } draw(params) { - params.uniforms = Object.assign({}, params.uniforms, { + params.uniforms = { + ...params.uniforms, timeRange: this.props.timeRange - }); + }; super.draw(params); } } - -AnimatedArcLayer.layerName = 'AnimatedArcLayer'; -AnimatedArcLayer.defaultProps = { - getSourceTimestamp: {type: 'accessor', value: 0}, - getTargetTimestamp: {type: 'accessor', value: 1}, - timeRange: {type: 'array', compare: true, value: [0, 1]} -}; diff --git a/examples/website/globe/app.jsx b/examples/website/globe/app.tsx similarity index 72% rename from examples/website/globe/app.jsx rename to examples/website/globe/app.tsx index c8b64eb5dd3..0ffc9f00578 100644 --- a/examples/website/globe/app.jsx +++ b/examples/website/globe/app.tsx @@ -20,11 +20,12 @@ import {CSVLoader} from '@loaders.gl/csv'; import AnimatedArcLayer from './animated-arc-group-layer'; import RangeInput from './range-input'; +import type {GlobeViewState} from '@deck.gl/core'; // Data source const DATA_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/globe'; -const INITIAL_VIEW_STATE = { +const INITIAL_VIEW_STATE: GlobeViewState = { longitude: 0, latitude: 20, zoom: 0 @@ -46,16 +47,36 @@ const sunLight = new SunLight({ // create lighting effect with light sources const lightingEffect = new LightingEffect({ambientLight, sunLight}); -/* eslint-disable react/no-deprecated */ -export default function App({data}) { +type Flight = { + // Departure + time1: number; + lon1: number; + lat1: number; + alt1: number; + + // Arrival + time2: number; + lon2: number; + lat2: number; + alt2: number; +}; + +type DailyFlights = { + date: string; + flights: Flight[]; +}; + +export default function App({data}: { + data?: DailyFlights[] +}) { const [currentTime, setCurrentTime] = useState(0); - const timeRange = [currentTime, currentTime + TIME_WINDOW]; + const timeRange: [number, number] = [currentTime, currentTime + TIME_WINDOW]; - const formatLabel = useCallback(t => getDate(data, t).toUTCString(), [data]); + const formatLabel = useCallback((t: number) => getDate(data, t).toUTCString(), [data]); if (data) { - sunLight.timestamp = getDate(data, currentTime).getTime(); + sunLight.timestamp = getDate(data, currentTime); } const backgroundLayers = useMemo( @@ -85,7 +106,7 @@ export default function App({data}) { data && data.map( ({date, flights}) => - new AnimatedArcLayer({ + new AnimatedArcLayer({ id: `flights-${date}`, data: flights, getSourcePosition: d => [d.lon1, d.lat1, d.alt1], @@ -123,35 +144,18 @@ export default function App({data}) { ); } -function getDate(data, t) { +function getDate(data: DailyFlights[], t: number) { const index = Math.min(data.length - 1, Math.floor(t / SEC_PER_DAY)); const date = data[index].date; const timestamp = new Date(`${date}T00:00:00Z`).getTime() + (t % SEC_PER_DAY) * 1000; return new Date(timestamp); } -export function renderToDOM(container) { +export async function renderToDOM(container: HTMLDivElement) { const root = createRoot(container); root.render(); - async function loadData(dates) { - const data = []; - for (const date of dates) { - const url = `${DATA_URL}/${date}.csv`; - const flights = await load(url, CSVLoader, {csv: {skipEmptyLines: true}}); - - // Join flight data from multiple dates into one continuous animation - const offset = SEC_PER_DAY * data.length; - for (const f of flights) { - f.time1 += offset; - f.time2 += offset; - } - data.push({flights, date}); - root.render(); - } - } - - loadData([ + const dates = [ '2020-01-14', '2020-02-11', '2020-03-10', @@ -164,5 +168,20 @@ export function renderToDOM(container) { '2020-10-13', '2020-11-10', '2020-12-08' - ]); + ]; + + const data: DailyFlights[] = []; + for (const date of dates) { + const url = `${DATA_URL}/${date}.csv`; + const flights: Flight[] = (await load(url, CSVLoader, {csv: {skipEmptyLines: true}})).data; + + // Join flight data from multiple dates into one continuous animation + const offset = SEC_PER_DAY * data.length; + for (const f of flights) { + f.time1 += offset; + f.time2 += offset; + } + data.push({flights, date}); + root.render(); + } } diff --git a/examples/website/globe/index.html b/examples/website/globe/index.html index 21e34f8ec04..d91d4471749 100644 --- a/examples/website/globe/index.html +++ b/examples/website/globe/index.html @@ -12,7 +12,7 @@
diff --git a/examples/website/globe/package.json b/examples/website/globe/package.json index 816bb915204..a60e345ad8d 100644 --- a/examples/website/globe/package.json +++ b/examples/website/globe/package.json @@ -12,6 +12,8 @@ "@loaders.gl/csv": "^4.1.4", "@material-ui/core": "^4.10.2", "@material-ui/icons": "^4.9.1", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "deck.gl": "^9.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/examples/website/globe/range-input.jsx b/examples/website/globe/range-input.tsx similarity index 81% rename from examples/website/globe/range-input.jsx rename to examples/website/globe/range-input.tsx index 8346ab6445c..a7d2ea6bc0a 100644 --- a/examples/website/globe/range-input.jsx +++ b/examples/website/globe/range-input.tsx @@ -30,7 +30,14 @@ const SliderInput = withStyles({ } })(Slider); -export default function RangeInput({min, max, value, animationSpeed, onChange, formatLabel}) { +export default function RangeInput({min, max, value, animationSpeed, onChange, formatLabel}: { + min: number; + max: number; + value: number; + animationSpeed: number; + formatLabel: (x: number) => string; + onChange: (newValue: number) => void; +}) { const [isPlaying, setIsPlaying] = useState(false); // prettier-ignore @@ -52,14 +59,14 @@ export default function RangeInput({min, max, value, animationSpeed, onChange, f return ( - onChange(newValue)} + onChange={(event, newValue) => onChange(newValue as number)} valueLabelDisplay="on" valueLabelFormat={formatLabel} /> diff --git a/examples/website/globe/tsconfig.json b/examples/website/globe/tsconfig.json new file mode 100644 index 00000000000..9b3c020493c --- /dev/null +++ b/examples/website/globe/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/examples/website/mask-extension/animated-arc-group-layer.js b/examples/website/mask-extension/animated-arc-group-layer.ts similarity index 61% rename from examples/website/mask-extension/animated-arc-group-layer.js rename to examples/website/mask-extension/animated-arc-group-layer.ts index 65b095f29cc..e272752d815 100644 --- a/examples/website/mask-extension/animated-arc-group-layer.js +++ b/examples/website/mask-extension/animated-arc-group-layer.ts @@ -1,18 +1,32 @@ -import {CompositeLayer} from '@deck.gl/core'; -import AnimatedArcLayer from './animated-arc-layer'; +import {CompositeLayer, UpdateParameters} from '@deck.gl/core'; +import AnimatedArcLayer, {AnimatedArcLayerProps} from './animated-arc-layer'; const MAX_ARCS_PER_LAYER = 2500; +type ArcsGroup = { + startTime: number; + endTime: number; + data: DataT[]; +}; + /** Same effect as the AnimatedArcLayer, but perf optimized. * Data is divided into smaller groups, and one sub layer is rendered for each group. * This allows us to cheaply cull invisible arcs by turning layers off and on. */ -export default class AnimatedArcGroupLayer extends CompositeLayer { - updateState({props, changeFlags}) { +export default class AnimatedArcGroupLayer extends CompositeLayer>> { + layerName = 'AnimatedArcGroupLayer'; + defaultProps = AnimatedArcLayer.defaultProps; + + state!: { + groups: ArcsGroup[]; + }; + + updateState({props, changeFlags}: UpdateParameters) { if (changeFlags.dataChanged) { // Sort and group data const {data, getSourceTimestamp, getTargetTimestamp} = props; - const groups = sortAndGroup(data, getSourceTimestamp, getTargetTimestamp, MAX_ARCS_PER_LAYER); + // @ts-ignore + const groups = sortAndGroup(data, getSourceTimestamp, getTargetTimestamp); this.setState({groups}); } } @@ -36,12 +50,14 @@ export default class AnimatedArcGroupLayer extends CompositeLayer { } } -AnimatedArcGroupLayer.layerName = 'AnimatedArcGroupLayer'; -AnimatedArcGroupLayer.defaultProps = AnimatedArcLayer.defaultProps; - -function sortAndGroup(data, getStartTime, getEndTime, groupSize) { - const groups = []; - let group = null; +function sortAndGroup( + data: DataT[], + getStartTime: (d: DataT) => number, + getEndTime: (d: DataT) => number, + groupSize: number = MAX_ARCS_PER_LAYER +): ArcsGroup[] { + const groups: ArcsGroup[] = []; + let group: ArcsGroup; data.sort((d1, d2) => getStartTime(d1) - getStartTime(d2)); diff --git a/examples/website/mask-extension/animated-arc-layer.js b/examples/website/mask-extension/animated-arc-layer.ts similarity index 66% rename from examples/website/mask-extension/animated-arc-layer.js rename to examples/website/mask-extension/animated-arc-layer.ts index 746dc493982..507ec9e5303 100644 --- a/examples/website/mask-extension/animated-arc-layer.js +++ b/examples/website/mask-extension/animated-arc-layer.ts @@ -1,6 +1,24 @@ -import {ArcLayer} from '@deck.gl/layers'; +import {ArcLayer, ArcLayerProps} from '@deck.gl/layers'; +import {Accessor, DefaultProps} from '@deck.gl/core'; + +export type AnimatedArcLayerProps = _AnimatedArcLayerProps & ArcLayerProps; + +type _AnimatedArcLayerProps = { + getSourceTimestamp?: Accessor; + getTargetTimestamp?: Accessor; + timeRange?: [number, number]; +}; + +const defaultProps: DefaultProps<_AnimatedArcLayerProps> = { + getSourceTimestamp: {type: 'accessor', value: 0}, + getTargetTimestamp: {type: 'accessor', value: 1}, + timeRange: {type: 'array', compare: true, value: [0, 1]} +}; + +export default class AnimatedArcLayer extends ArcLayer> { + layerName = 'AnimatedArcLayer'; + defaultProps = defaultProps; -export default class AnimatedArcLayer extends ArcLayer { getShaders() { const shaders = super.getShaders(); shaders.inject = { @@ -49,16 +67,10 @@ color.a *= smoothstep(1.1 * w, w, abs(geometry.uv.y)); } draw(params) { - params.uniforms = Object.assign({}, params.uniforms, { + params.uniforms = { + ...params.uniforms, timeRange: this.props.timeRange - }); + }; super.draw(params); } } - -AnimatedArcLayer.layerName = 'AnimatedArcLayer'; -AnimatedArcLayer.defaultProps = { - getSourceTimestamp: {type: 'accessor', value: 0}, - getTargetTimestamp: {type: 'accessor', value: 1}, - timeRange: {type: 'array', compare: true, value: [0, 1]} -}; diff --git a/examples/website/mask-extension/app.jsx b/examples/website/mask-extension/app.tsx similarity index 79% rename from examples/website/mask-extension/app.jsx rename to examples/website/mask-extension/app.tsx index 3fd23ebe955..f75a78e46a0 100644 --- a/examples/website/mask-extension/app.jsx +++ b/examples/website/mask-extension/app.tsx @@ -12,19 +12,35 @@ import {CSVLoader} from '@loaders.gl/csv'; import AnimatedArcLayer from './animated-arc-group-layer'; import RangeInput from './range-input'; +import type {MapViewState} from '@deck.gl/core'; +import type {AnimatedArcLayerProps} from './animated-arc-layer'; // Data source const DATA_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/globe/2020-01-14.csv'; const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json'; -const INITIAL_VIEW_STATE = { +const INITIAL_VIEW_STATE: MapViewState = { longitude: -40, latitude: 40, zoom: 2, maxZoom: 6 }; +type Flight = { + // Departure + time1: number; + lon1: number; + lat1: number; + alt1: number; + + // Arrival + time2: number; + lon2: number; + lat2: number; + alt2: number; +}; + /* eslint-disable react/no-deprecated */ export default function App({ data, @@ -32,6 +48,12 @@ export default function App({ showFlights = true, timeWindow = 30, animationSpeed = 3 +}: { + data?: Flight[]; + mapStyle?: string; + showFlights?: boolean; + timeWindow?: number; + animationSpeed?: number; }) { const [currentTime, setCurrentTime] = useState(0); @@ -68,7 +90,7 @@ export default function App({ [] ); - const flightLayerProps = { + const flightLayerProps: Partial> = { data, greatCircle: true, getSourcePosition: d => [d.lon1, d.lat1], @@ -80,7 +102,7 @@ export default function App({ const flightPathsLayer = showFlights && - new AnimatedArcLayer({ + new AnimatedArcLayer({ ...flightLayerProps, id: 'flight-paths', timeRange: [currentTime - 600, currentTime], // 10 minutes @@ -90,10 +112,10 @@ export default function App({ widthUnits: 'common', getSourceColor: [180, 232, 255], getTargetColor: [180, 232, 255], - parameters: {depthTest: false} + parameters: {depthCompare: 'always'} }); - const flightMaskLayer = new AnimatedArcLayer({ + const flightMaskLayer = new AnimatedArcLayer({ ...flightLayerProps, id: 'flight-mask', timeRange: [currentTime - timeWindow * 60, currentTime], @@ -125,18 +147,17 @@ export default function App({ ); } -function formatTimeLabel(seconds) { +function formatTimeLabel(seconds: number) { const h = Math.floor(seconds / 3600); const m = Math.floor(seconds / 60) % 60; const s = seconds % 60; return [h, m, s].map(x => x.toString().padStart(2, '0')).join(':'); } -export function renderToDOM(container) { +export async function renderToDOM(container: HTMLDivElement) { const root = createRoot(container); root.render(); - load(DATA_URL, CSVLoader).then(flights => { - root.render(); - }); + const flights = (await load(DATA_URL, CSVLoader)).data; + root.render(); } diff --git a/examples/website/mask-extension/index.html b/examples/website/mask-extension/index.html index 21e34f8ec04..d91d4471749 100644 --- a/examples/website/mask-extension/index.html +++ b/examples/website/mask-extension/index.html @@ -12,7 +12,7 @@
diff --git a/examples/website/mask-extension/package.json b/examples/website/mask-extension/package.json index 6bf1e66c378..790a191f37f 100644 --- a/examples/website/mask-extension/package.json +++ b/examples/website/mask-extension/package.json @@ -12,6 +12,8 @@ "@loaders.gl/csv": "^4.1.4", "@material-ui/core": "^4.10.2", "@material-ui/icons": "^4.9.1", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "deck.gl": "^9.0.0", "maplibre-gl": "^3.0.0", "react": "^18.0.0", diff --git a/examples/website/mask-extension/range-input.jsx b/examples/website/mask-extension/range-input.tsx similarity index 74% rename from examples/website/mask-extension/range-input.jsx rename to examples/website/mask-extension/range-input.tsx index 44f4a9de781..844902cef94 100644 --- a/examples/website/mask-extension/range-input.jsx +++ b/examples/website/mask-extension/range-input.tsx @@ -12,29 +12,35 @@ const PositionContainer = styled('div')({ bottom: '40px', width: '100%', display: 'flex', + color: '#f5f1d8', justifyContent: 'center', alignItems: 'center' }); -const COLOR = '#f5f1d8'; - const SliderInput = withStyles({ root: { marginLeft: 12, width: '40%', - color: COLOR + color: '#f5f1d8' }, valueLabel: { '& span': { whiteSpace: 'nowrap', background: 'none', - color: COLOR + color: '#f5f1d8' } } })(Slider); -export default function RangeInput({min, max, value, animationSpeed, onChange, formatLabel}) { - const [isPlaying, setIsPlaying] = useState(true); +export default function RangeInput({min, max, value, animationSpeed, onChange, formatLabel}: { + min: number; + max: number; + value: number; + animationSpeed: number; + formatLabel: (x: number) => string; + onChange: (newValue: number) => void; +}) { + const [isPlaying, setIsPlaying] = useState(false); // prettier-ignore useEffect(() => { @@ -54,15 +60,15 @@ export default function RangeInput({min, max, value, animationSpeed, onChange, f }); return ( - - onChange(newValue)} + onChange={(event, newValue) => onChange(newValue as number)} valueLabelDisplay="on" valueLabelFormat={formatLabel} /> diff --git a/examples/website/mask-extension/tsconfig.json b/examples/website/mask-extension/tsconfig.json new file mode 100644 index 00000000000..9b3c020493c --- /dev/null +++ b/examples/website/mask-extension/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/examples/website/orthographic/app.tsx b/examples/website/orthographic/app.tsx index 84733d50456..501efac57e4 100644 --- a/examples/website/orthographic/app.tsx +++ b/examples/website/orthographic/app.tsx @@ -1,17 +1,18 @@ import React from 'react'; import {useMemo} from 'react'; - import {createRoot} from 'react-dom/client'; import {OrthographicView} from '@deck.gl/core'; import {TextLayer, PathLayer} from '@deck.gl/layers'; import {SimpleMeshLayer} from '@deck.gl/mesh-layers'; import DeckGL from '@deck.gl/react'; import {Matrix4} from '@math.gl/core'; -import type {MeshAttributes} from '@loaders.gl/schema'; - import {scaleLinear} from 'd3-scale'; +import {minIndex, maxIndex} from 'd3-array'; import {sortData} from './sort-data'; +import type {MeshAttributes} from '@loaders.gl/schema'; +import type {Color, Position, OrthographicViewState, PickingInfo} from '@deck.gl/core'; + // Data source const DATA_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/ghcn/ghcn-annual.json'; @@ -27,6 +28,22 @@ const borderMesh: MeshAttributes = { size: 3 } }; + +export type Station = { + id: string; + country: string; + name: string; + longitude: number; + latitude: number; + altitude: number; + meanTemp: [year: number, temperature: number][]; +}; + +export type StationGroup = { + name: string; + stations: Station[]; +}; + const xScale = scaleLinear() .domain([1880, 2020]) // year .range([0, CHART_WIDTH]); @@ -35,7 +52,7 @@ const yScale = scaleLinear() .domain([-60, 35]) // temperature .range([CHART_HEIGHT, 0]); const yTicks = [-60, -30, 0, 30]; -export const colorScale = scaleLinear<[number, number, number]>() +export const colorScale = scaleLinear() .domain([-60, -10, 30]) // temperature .range([ [80, 160, 225], @@ -43,62 +60,69 @@ export const colorScale = scaleLinear<[number, number, number]>() [255, 80, 80] ]); -function getOffset(chartIndex: number) { - const y = Math.floor(chartIndex / ROW_SIZE); - const x = chartIndex % ROW_SIZE; +function getPlotOffset(plotIndex: number): [number, number, number] { + const y = Math.floor(plotIndex / ROW_SIZE); + const x = plotIndex % ROW_SIZE; return [x * (CHART_WIDTH + SPACING), y * (CHART_HEIGHT + SPACING), 0]; } -function getTooltip({object}) { - return ( - object && - `\ - ${object.name} - ${object.country} - Latitude: ${Math.abs(object.latitude)}°${object.latitude >= 0 ? 'N' : 'S'}, - Altitude: ${object.altitude === null ? 'N/A' : object.altitude}, - Lowest: ${object.min[1]}°C in ${object.min[0]} - Highest: ${object.max[1]}°C in ${object.max[0]} +function getTooltip({object}: PickingInfo) { + if (!object) return null; + + const {meanTemp, name, country, latitude, altitude} = object; + const minYear = meanTemp[minIndex(meanTemp, d => d[1])]; + const maxYear = meanTemp[maxIndex(meanTemp, d => d[1])]; + + return (`\ + ${name} + ${country} + Latitude: ${Math.abs(latitude)}°${latitude >= 0 ? 'N' : 'S'} + Altitude: ${altitude === null ? 'N/A' : altitude} + Lowest: ${minYear[1]}°C in ${minYear[0]} + Highest: ${maxYear[1]}°C in ${maxYear[0]} ` ); } -export default function App({data = null, groupBy = 'Country'}) { - const dataSlices = useMemo(() => sortData(data, groupBy), [data, groupBy]); +export default function App({data, groupBy = 'Country'}: { + data?: Station[]; + groupBy?: 'Country' | 'Latitude'; +}) { + const plots: StationGroup[] = useMemo(() => sortData(data, groupBy), [data, groupBy]); - const initialViewState = useMemo(() => { - const centerX = (Math.min(dataSlices.length, ROW_SIZE) / 2) * (CHART_WIDTH + SPACING); - const centerY = (Math.ceil(dataSlices.length / ROW_SIZE) / 2) * (CHART_HEIGHT + SPACING); + const initialViewState: OrthographicViewState = useMemo(() => { + const centerX = (Math.min(plots.length, ROW_SIZE) / 2) * (CHART_WIDTH + SPACING); + const centerY = (Math.ceil(plots.length / ROW_SIZE) / 2) * (CHART_HEIGHT + SPACING); return { target: [centerX, centerY, 0], zoom: -2, minZoom: -2 }; - }, [dataSlices.length]); + }, [plots.length]); const yLabels = useMemo( () => - dataSlices.flatMap((_, i) => { - return yTicks.map(y => ({index: i, y})); + plots.flatMap((_, i) => { + return yTicks.map(y => ({plotIndex: i, y})); }), - [dataSlices.length] + [plots.length] ); const xLabels = useMemo( () => - dataSlices.flatMap((_, i) => { - return xTicks.map(x => ({index: i, x})); + plots.flatMap((_, i) => { + return xTicks.map(x => ({plotIndex: i, x})); }), - [dataSlices.length] + [plots.length] ); const layers = [ - dataSlices.map( - (slice, i) => - new PathLayer({ + plots.map( + (slice: StationGroup, i: number) => + new PathLayer({ id: slice.name, data: slice.stations, - modelMatrix: new Matrix4().translate(getOffset(i)), - getPath: d => d.meanTemp.map(p => [xScale(p[0]), yScale(p[1])]), + modelMatrix: new Matrix4().translate(getPlotOffset(i)), + getPath: d => d.meanTemp.map(p => [xScale(p[0]), yScale(p[1])] as Position), getColor: d => d.meanTemp.map(p => colorScale(p[1])), getWidth: 1, widthMinPixels: 1, @@ -109,20 +133,20 @@ export default function App({data = null, groupBy = 'Country'}) { highlightColor: [255, 200, 0, 255] }) ), - new SimpleMeshLayer({ + new SimpleMeshLayer({ id: 'border', - data: dataSlices, + data: plots, mesh: borderMesh, - getPosition: (d, {index}) => getOffset(index), + getPosition: (d, {index}) => getPlotOffset(index), getScale: [CHART_WIDTH, CHART_HEIGHT, 1], getColor: [255, 255, 255], wireframe: true }), - new TextLayer({ + new TextLayer<{plotIndex: number, y: number}>({ id: 'y-labels', data: yLabels, getPosition: d => { - const offset = getOffset(d.index); + const offset = getPlotOffset(d.plotIndex); return [-4 + offset[0], yScale(d.y) + offset[1]]; }, getText: d => String(d.y), @@ -132,11 +156,11 @@ export default function App({data = null, groupBy = 'Country'}) { sizeMaxPixels: 28, getTextAnchor: 'end' }), - new TextLayer({ + new TextLayer<{plotIndex: number, x: number}>({ id: 'x-labels', data: xLabels, getPosition: d => { - const offset = getOffset(d.index); + const offset = getPlotOffset(d.plotIndex); return [xScale(d.x) + offset[0], CHART_HEIGHT + offset[1] + 4]; }, getText: d => String(d.x), @@ -146,10 +170,10 @@ export default function App({data = null, groupBy = 'Country'}) { sizeMaxPixels: 28, getAlignmentBaseline: 'top' }), - new TextLayer({ + new TextLayer({ id: 'title', - data: dataSlices, - getPosition: (d, {index}) => getOffset(index), + data: plots, + getPosition: (d, {index}) => getPlotOffset(index), getText: d => d.name, getSize: 16, sizeUnits: 'meters', @@ -173,14 +197,12 @@ export default function App({data = null, groupBy = 'Country'}) { ); } -export function renderToDOM(container) { +export async function renderToDOM(container: HTMLDivElement) { const root = createRoot(container); root.render(); /* global fetch */ - fetch(DATA_URL) - .then(resp => resp.json()) - .then(data => { - root.render(); - }); + const resp = await fetch(DATA_URL); + const data = await resp.json(); + root.render(); } diff --git a/examples/website/orthographic/package.json b/examples/website/orthographic/package.json index 4f05cdc336d..72de89ff515 100644 --- a/examples/website/orthographic/package.json +++ b/examples/website/orthographic/package.json @@ -9,6 +9,11 @@ "build": "vite build" }, "dependencies": { + "@types/d3-array": "^3.2.0", + "@types/d3-scale": "^4.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "d3-array": "^3.2.0", "d3-scale": "^4.0.0", "deck.gl": "^9.0.0", "react": "^18.0.0", diff --git a/examples/website/orthographic/sort-data.js b/examples/website/orthographic/sort-data.ts similarity index 60% rename from examples/website/orthographic/sort-data.js rename to examples/website/orthographic/sort-data.ts index afea0c31d05..a0e7e19e37a 100644 --- a/examples/website/orthographic/sort-data.js +++ b/examples/website/orthographic/sort-data.ts @@ -1,11 +1,11 @@ -export function sortData(data, groupBy) { - if (!data) return []; +import type {Station, StationGroup} from './app'; - calculateRange(data); +export function sortData(data: Station[], groupBy: 'Country' | 'Latitude'): StationGroup[] { + if (!data) return []; switch (groupBy) { case 'Country': { - const dataByCountry = {}; + const dataByCountry: {[country: string]: StationGroup} = {}; for (const station of data) { const country = (dataByCountry[station.country] = dataByCountry[station.country] || { name: station.country, @@ -17,7 +17,7 @@ export function sortData(data, groupBy) { } case 'Latitude': { - const dataByLat = Array.from({length: 12}, (_, i) => ({ + const dataByLat: StationGroup[] = Array.from({length: 12}, (_, i) => ({ name: `${-90 + i * 15}°/${-75 + i * 15}°`, stations: [] })); @@ -32,23 +32,3 @@ export function sortData(data, groupBy) { throw new Error('Unknown groupBy mode'); } } - -function calculateRange(data) { - if (data[0].min) return; - - for (const station of data) { - let min = null; - let max = null; - - for (const p of station.meanTemp) { - if (!min || min[1] > p[1]) { - min = p; - } - if (!max || max[1] <= p[1]) { - max = p; - } - } - station.min = min; - station.max = max; - } -} diff --git a/examples/website/orthographic/tsconfig.json b/examples/website/orthographic/tsconfig.json new file mode 100644 index 00000000000..9b3c020493c --- /dev/null +++ b/examples/website/orthographic/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/examples/website/plot/app.jsx b/examples/website/plot/app.jsx deleted file mode 100644 index 7af46d844dc..00000000000 --- a/examples/website/plot/app.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import {createRoot} from 'react-dom/client'; -import DeckGL from '@deck.gl/react'; -import {OrbitView} from '@deck.gl/core'; -import PlotLayer from './plot-layer'; -import {scaleLinear} from 'd3-scale'; - -const EQUATION = (x, y) => (Math.sin(x * x + y * y) * x) / Math.PI; - -const INITIAL_VIEW_STATE = { - target: [0.5, 0.5, 0.5], - rotationX: 30, - rotationOrbit: -30, - /* global window */ - zoom: typeof window !== `undefined` ? Math.log2(window.innerHeight / 3) : 1 // fit 3x3x3 box in current viewport -}; - -function getScale({min, max}) { - return scaleLinear().domain([min, max]).range([0, 1]); -} - -function getTooltip({sample}) { - return sample && sample.map(x => x.toFixed(3)).join(', '); -} - -export default function App({resolution = 200, showAxis = true, equation = EQUATION}) { - const layers = [ - equation && - resolution && - new PlotLayer({ - getPosition: ([u, v]) => { - const x = (u - 1 / 2) * Math.PI * 2; - const y = (v - 1 / 2) * Math.PI * 2; - return [x, y, equation(x, y)]; - }, - getColor: ([x, y, z]) => [40, z * 128 + 128, 160], - getXScale: getScale, - getYScale: getScale, - getZScale: getScale, - uCount: resolution, - vCount: resolution, - drawAxes: showAxis, - axesPadding: 0.25, - axesColor: [0, 0, 0, 128], - pickable: true, - updateTriggers: { - getPosition: equation - } - }) - ]; - - return ( - - ); -} - -export function renderToDOM(container) { - createRoot(container).render(); -} diff --git a/examples/website/plot/app.tsx b/examples/website/plot/app.tsx new file mode 100644 index 00000000000..720996f5b04 --- /dev/null +++ b/examples/website/plot/app.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import {createRoot} from 'react-dom/client'; +import DeckGL from '@deck.gl/react'; +import {OrbitView, OrbitViewState} from '@deck.gl/core'; +import PlotLayer, {Axes, PlotLayerPickingInfo} from './plot-layer'; +import {scaleLinear} from 'd3-scale'; + +/** Given x and y, returns z coordinate */ +type Equation = (x: number, y: number) => number; + +const DEFAULT_EQUATION = (x, y) => (Math.sin(x * x + y * y) * x) / Math.PI; +const MAX_SIZE = 24; + +const INITIAL_VIEW_STATE: OrbitViewState = { + target: [0, 0, 0], + rotationX: 30, + rotationOrbit: -30, + /* global window */ + zoom: typeof window !== `undefined` ? Math.log2(window.innerHeight / MAX_SIZE) + 1 : 1 +}; + +function onAxesChange(axes: Axes) { + for (const axis of Object.values(axes)) { + let size = axis.max - axis.min; + let clamped = false; + if (size > MAX_SIZE) { + const mid = (axis.min + axis.max) / 2; + axis.min = mid - MAX_SIZE / 2; + axis.max = mid + MAX_SIZE / 2; + size = MAX_SIZE; + clamped = true; + } + + const scale = scaleLinear() + .domain([axis.min, axis.max]) + .range([-size / 2, size / 2]) + .clamp(clamped) + .nice(); + axis.min = scale.domain()[0]; + axis.max = scale.domain()[1]; + axis.scale = scale; + axis.ticks = scale.ticks(6); + } +} + +function getTooltip({sample}: PlotLayerPickingInfo) { + return sample && sample.map(x => x.toFixed(3)).join(', '); +} + +export default function App({ + resolution = 200, + showAxis = true, + equation = DEFAULT_EQUATION +}: { + equation?: Equation; + resolution?: number; + showAxis?: boolean; +}) { + const layers = [ + equation && + resolution && + new PlotLayer({ + getPosition: ([u, v]) => { + const x = (u - 1 / 2) * Math.PI * 2; + const y = (v - 1 / 2) * Math.PI * 2; + return [x, y, equation(x, y)]; + }, + getColor: ([x, y, z]) => [40, z * 128 + 128, 160], + onAxesChange, + uCount: resolution, + vCount: resolution, + drawAxes: showAxis, + axesPadding: 0.25, + axesColor: [0, 0, 0, 128], + pickable: true, + updateTriggers: { + getPosition: equation + } + }) + ]; + + return ( + + ); +} + +export function renderToDOM(container: HTMLDivElement) { + createRoot(container).render(); +} diff --git a/examples/website/plot/index.html b/examples/website/plot/index.html index 4620f478849..7900f2a7ff0 100644 --- a/examples/website/plot/index.html +++ b/examples/website/plot/index.html @@ -12,7 +12,7 @@
diff --git a/examples/website/plot/package.json b/examples/website/plot/package.json index 8b4379d8926..ed19f7add16 100644 --- a/examples/website/plot/package.json +++ b/examples/website/plot/package.json @@ -9,6 +9,9 @@ "build": "vite build" }, "dependencies": { + "@types/d3-scale": "^4.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "d3-scale": "^4.0.0", "deck.gl": "^9.0.0", "react": "^18.0.0", diff --git a/examples/website/plot/plot-layer/README.md b/examples/website/plot/plot-layer/README.md index 57604ac63d2..4da684a6621 100644 --- a/examples/website/plot/plot-layer/README.md +++ b/examples/website/plot/plot-layer/README.md @@ -11,7 +11,7 @@ Inherits from all [Base Layer](/docs/layers/base-layer.md) properties. ##### `getPosition` (Function, optional) -- Default: `(u, v) => [0, 0, 0]` +- Default: `([u, v]) => [0, 0, 0]` Called to get the `[x, y, z]` value from a `(u, v)` pair. @@ -19,70 +19,59 @@ Arguments: - `u` (Number) - a value between `[0, 1]` - `v` (Number) - a value between `[0, 1]` -##### `getColor` (Function, optional) +##### `getColor` (Function | Color, optional) -- Default: `(x, y, z) => [0, 0, 0, 255]` +- Default: `[128, 128, 128, 255]` -Called for each `(x, y, z)` triplet to retreive surface color. -Returns an array in the form of `[r, g, b, a]`. -If the alpha component is not supplied, it is default to `255`. +Color of the surface. +If a function is supplied, it is called for each `[x, y, z]` position to retreive surface color. +Returns an array in the form of `[r, g, b, a]`. If the alpha component is not supplied, it is default to `255`. -##### `getXScale` (Function, optional) +##### `onAxesChange` (Function, optional) -- Default: `({min, max}) => d3.scaleLinear()` +- Default: `(axes: Axes) => void` -Called to retreive a [d3 scale](https://github.com/d3/d3-scale/blob/master/README.md) for x values. +Called to get optional settings for each axis. Default to identity. Arguments: -- `context` (Object) - + `context.min` (Number) - lower bounds of x values - + `context.max` (Number) - upper bounds of x values +- `axes` (Axes) + + `x` (Axis) + + `y` (Axis) + + `z` (Axis) -##### `getYScale` (Function, optional) +Each Axis object contains the following fields: -- Default: `({min, max}) => d3.scaleLinear()` +- `name` (string) - one of `'x'`, `'y'` or `'z'` +- `min` (number) - lower bound of the values on this axis +- `max` (number) - upper bound of the values on this axis -Called to retreive a [d3 scale](https://github.com/d3/d3-scale/blob/master/README.md) for y values. -Default to identity. - -Arguments: -- `context` (Object) - + `context.min` (Number) - lower bounds of y values - + `context.max` (Number) - upper bounds of y values - -##### `getZScale` (Function, optional) - -- Default: `({min, max}) => d3.scaleLinear()` +The callback offers an oportunity to populate an axis with some optional fields: -Called to retreive a [d3 scale](https://github.com/d3/d3-scale/blob/master/README.md) for z values. -Default to identity. - -Arguments: -- `context` (Object) - + `context.min` (Number) - lower bounds of z values - + `context.max` (Number) - upper bounds of z values +- `title` (string) +- `scale` (Function) - remaps values in the model space +- `ticks` (number[]) - list of values at which to display grid/labels -##### `uCount` (Number, optional) +##### `uCount` (number, optional) - Default: `100` Number of points to sample `u` in the `[0, 1]` range. -##### `vCount` (Number, optional) +##### `vCount` (number, optional) - Default: `100` Number of points to sample `v` in the `[0, 1]` range. -##### `lightStrength` (Number, optional) +##### `lightStrength` (number, optional) - Default: `0.1` Intensity of the front-lit effect for the 3d surface. -##### `drawAxes` (Bool, optional) +##### `drawAxes` (boolean, optional) - Default: `true` @@ -94,75 +83,20 @@ Whether to draw axis grids and labels. Font size of the labels. -##### `xTicks` (Number | [Number], optional) - -- Default: `6` - -Either number of ticks on x axis, or an array of tick values. - -##### `yTicks` (Number | [Number], optional) - -- Default: `6` - -Either number of ticks on y axis, or an array of tick values. - -##### `zTicks` (Number | [Number], optional) - -- Default: `6` - -Either number of ticks on z axis, or an array of tick values. - - -##### `xTickFormat` (Function, optional) - -- Default: `value => value.toFixed(2)` - -Format a tick value on x axis to text string. - -##### `yTickFormat` (Function, optional) - -- Default: `value => value.toFixed(2)` - -Format a tick value on y axis to text string. - -##### `zTickFormat` (Function, optional) +##### `tickFormat` (Function, optional) - Default: `value => value.toFixed(2)` -Format a tick value on z axis to text string. - -##### `xTitle` (String, optional) - -- Default: `x` - -X axis title string. +Format a tick value on an axis to text string. -##### `yTitle` (String, optional) - -- Default: `y` - -Y axis title string. - -##### `zTitle` (String, optional) - -- Default: `z` - -Z axis title string. - -##### `axesPadding` (Number, optional) +##### `axesPadding` (number, optional) - Default: `0` Amount that grids should setback from the bounding box. Relative to the size of the bounding box. -##### `axesColor` (Array, optional) +##### `axesColor` (Color, optional) - Default: `[0, 0, 0, 255]` Color to draw the grids with, in `[r, g, b, a]`. - -##### `axesTitles` (Array, optional) - -- Default: `['x', 'z', 'y']` - -Strings to draw next to each axis as their titles, note that the second element is the axis that points upwards. \ No newline at end of file diff --git a/examples/website/plot/plot-layer/axes-layer.ts b/examples/website/plot/plot-layer/axes-layer.ts index cb16654cb58..71feccb1570 100644 --- a/examples/website/plot/plot-layer/axes-layer.ts +++ b/examples/website/plot/plot-layer/axes-layer.ts @@ -1,149 +1,95 @@ -import {Color, DefaultProps, Layer, LayerDataSource, LayerProps} from '@deck.gl/core'; +import {Color, DefaultProps, Layer, UpdateParameters, Attribute, LayerProps} from '@deck.gl/core'; import {Model, Geometry} from '@luma.gl/engine'; import {Texture} from '@luma.gl/core'; -import {ScaleLinear} from 'd3-scale'; import {textMatrixToTexture} from './utils'; -import fragmentShader from './axes-fragment.glsl'; +import gridFragment from './grid-fragment.glsl'; import gridVertex from './grid-vertex.glsl'; import labelVertex from './label-vertex.glsl'; import labelFragment from './label-fragment.glsl'; -import {Axis, Tick, TickFormat, Vec2, Vec3} from './types'; +import {Axis, TickFormat, Vec3} from './types'; -/* Constants */ -const DEFAULT_FONT_SIZE = 48; -const DEFAULT_TICK_COUNT = 6; -const DEFAULT_TICK_FORMAT = (x: number) => x.toFixed(2); -const DEFAULT_COLOR: Color = [0, 0, 0, 255]; +type Tick = { + axis: 'x' | 'y' | 'z'; + value: string; + position: [number, number]; + text: string; +}; interface LabelTexture { labelHeight: number; labelWidths: number[]; - labelTextureDim: Vec2; labelTexture: Texture; -} +}; const defaultProps: DefaultProps = { - data: [], fontSize: 12, - xScale: undefined, - yScale: undefined, - zScale: undefined, - xTicks: DEFAULT_TICK_COUNT, - yTicks: DEFAULT_TICK_COUNT, - zTicks: DEFAULT_TICK_COUNT, - xTickFormat: DEFAULT_TICK_FORMAT as TickFormat, - yTickFormat: DEFAULT_TICK_FORMAT as TickFormat, - zTickFormat: DEFAULT_TICK_FORMAT as TickFormat, + tickFormat: {type: 'function', value: (x: number) => x.toFixed(2)}, padding: 0, - color: DEFAULT_COLOR, - xTitle: 'x', - yTitle: 'y', - zTitle: 'z' + color: {type: 'color', value: [0, 0, 0, 255]} }; /** All props supported by AxesLayer. */ -export type AxesLayerProps = _AxesLayerProps & LayerProps; - -type _AxesLayerProps = { - data: LayerDataSource; - fontSize: number; - xScale: ScaleLinear; - yScale: ScaleLinear; - zScale: ScaleLinear; - xTicks: number; - yTicks: number; - zTicks: number; - xTickFormat: TickFormat; - yTickFormat: TickFormat; - zTickFormat: TickFormat; - padding: 0; - color: Color; - xTitle: string; - yTitle: string; - zTitle: string; +export type AxesLayerProps = _AxesLayerProps & LayerProps; + +type _AxesLayerProps = { + xAxis: Axis; + yAxis: Axis; + zAxis: Axis; + fontSize?: number; + tickFormat?: TickFormat; + padding?: number; + color?: Color; }; /* - * @classdesc * A layer that plots a surface based on a z=f(x,y) equation. - * - * @class - * @param {Object} [props] - * @param {Integer} [props.ticksCount] - number of ticks along each axis, see - https://github.com/d3/d3-axis/blob/master/README.md#axis_ticks - * @param {Number} [props.padding] - amount to set back grids from the plot, - relative to the size of the bounding box - * @param {d3.scale} [props.xScale] - a d3 scale for the x axis - * @param {d3.scale} [props.yScale] - a d3 scale for the y axis - * @param {d3.scale} [props.zScale] - a d3 scale for the z axis - * @param {Number | [Number]} [props.xTicks] - either tick counts or an array of tick values - * @param {Number | [Number]} [props.yTicks] - either tick counts or an array of tick values - * @param {Number | [Number]} [props.zTicks] - either tick counts or an array of tick values - * @param {Function} [props.xTickFormat] - returns a string from value - * @param {Function} [props.yTickFormat] - returns a string from value - * @param {Function} [props.zTickFormat] - returns a string from value - * @param {String} [props.xTitle] - x axis title - * @param {String} [props.yTitle] - y axis title - * @param {String} [props.zTitle] - z axis title - * @param {Number} [props.fontSize] - size of the labels - * @param {Array} [props.color] - color of the gridlines, in [r,g,b,a] */ -export default class AxesLayer extends Layer< - ExtraPropsT & Required<_AxesLayerProps> -> { +export default class AxesLayer extends Layer> { static layerName = 'AxesLayer'; static defaultProps = defaultProps; - state!: Layer['state'] & { + state!: { models: [Model, Model]; modelsByName: {grids: Model; labels: Model}; - numInstances: number; - ticks: [Tick[], Tick[], Tick[]]; + ticks: Tick[]; gridDims: Vec3; gridCenter: Vec3; labelTexture: LabelTexture | null; }; initializeState() { - const {gl} = this.context; const attributeManager = this.getAttributeManager()!; attributeManager.addInstanced({ instancePositions: {size: 2, update: this.calculateInstancePositions, noAlloc: true}, instanceNormals: {size: 3, update: this.calculateInstanceNormals, noAlloc: true}, - instanceIsTitle: {size: 1, update: this.calculateInstanceIsTitle, noAlloc: true} + instanceOffsets: {size: 1, update: this.calculateInstanceOffsets, noAlloc: true} }); - this.setState(Object.assign({numInstances: 0}, this._getModels(gl))); + this.setState(this._getModels()); } - updateState({oldProps, props, changeFlags}) { + updateState({oldProps, props}: UpdateParameters) { const attributeManager = this.getAttributeManager()!; if ( - oldProps.xScale !== props.xScale || - oldProps.yScale !== props.yScale || - oldProps.zScale !== props.zScale || - oldProps.xTicks !== props.xTicks || - oldProps.yTicks !== props.yTicks || - oldProps.zTicks !== props.zTicks || - oldProps.xTickFormat !== props.xTickFormat || - oldProps.yTickFormat !== props.yTickFormat || - oldProps.zTickFormat !== props.zTickFormat + oldProps.xAxis !== props.xAxis || + oldProps.yAxis !== props.yAxis || + oldProps.zAxis !== props.zAxis ) { - const {xScale, yScale, zScale} = props; + const {xAxis, yAxis, zAxis, tickFormat} = props; const ticks = [ - getTicks({...props, axis: 'x'}), - getTicks({...props, axis: 'z'}), - getTicks({...props, axis: 'y'}) + ...getTicks(xAxis, tickFormat), + ...getTicks(yAxis, tickFormat), + ...getTicks(zAxis, tickFormat) ]; - const xRange = xScale.range(); - const yRange = yScale.range(); - const zRange = zScale.range(); + const xRange = getRange(xAxis); + const yRange = getRange(yAxis); + const zRange = getRange(zAxis); this.setState({ ticks, @@ -161,7 +107,7 @@ export default class AxesLayer extends } draw({uniforms}) { - const {gridDims, gridCenter, modelsByName, numInstances} = this.state; + const {gridDims, gridCenter, modelsByName, ticks} = this.state; const {labelTexture, ...labelTextureUniforms} = this.state.labelTexture!; const {fontSize, color, padding} = this.props; @@ -174,21 +120,23 @@ export default class AxesLayer extends strokeColor: color }; - modelsByName.grids.setInstanceCount(numInstances); - modelsByName.labels.setInstanceCount(numInstances); + modelsByName.grids.setInstanceCount(ticks.length); + modelsByName.labels.setInstanceCount(ticks.length); - modelsByName.grids.setUniforms(Object.assign({}, uniforms, baseUniforms)); + modelsByName.grids.setUniforms({...uniforms, ...baseUniforms}); modelsByName.labels.setBindings({labelTexture}); - modelsByName.labels.setUniforms( - Object.assign({}, uniforms, baseUniforms, labelTextureUniforms) - ); + modelsByName.labels.setUniforms({ + ...uniforms, + ...baseUniforms, + ...labelTextureUniforms + }); modelsByName.grids.draw(this.context.renderPass); modelsByName.labels.draw(this.context.renderPass); } } - _getModels(gl) { + _getModels() { /* grids: * for each x tick, draw rectangle on yz plane around the bounding box. * for each y tick, draw rectangle on zx plane around the bounding box. @@ -230,11 +178,13 @@ export default class AxesLayer extends // bottom edge 0, -1, 0, 0, -1, 0 ]; + const {device} = this.context; - const grids = new Model(gl.device, { + const grids = new Model(device, { id: `${this.props.id}-grids`, vs: gridVertex, - fs: fragmentShader, + fs: gridFragment, + disableWarnings: true, bufferLayout: this.getAttributeManager()!.getBufferLayouts(), geometry: new Geometry({ topology: 'line-list', @@ -280,11 +230,12 @@ export default class AxesLayer extends } } - const labels = new Model(gl.device, { + const labels = new Model(device, { id: `${this.props.id}-labels`, vs: labelVertex, fs: labelFragment, bufferLayout: this.getAttributeManager()!.getBufferLayouts(), + disableWarnings: true, geometry: new Geometry({ topology: 'triangle-list', attributes: { @@ -302,57 +253,68 @@ export default class AxesLayer extends }; } - calculateInstancePositions(attribute) { + calculateInstancePositions(attribute: Attribute) { const {ticks} = this.state; - const positions = ticks.map(axisTicks => axisTicks.map((t, i) => [t.position, i])); - - const value = new Float32Array(flatten(positions)); - attribute.value = value; - - this.setState({numInstances: value.length / attribute.size}); + const positions = ticks.flatMap(t => t.position); + attribute.value = new Float32Array(positions); } - calculateInstanceNormals(attribute) { - const { - ticks: [xTicks, zTicks, yTicks] - } = this.state; - - const normals = [ - xTicks.map(t => [1, 0, 0]), - zTicks.map(t => [0, 1, 0]), - yTicks.map(t => [0, 0, 1]) - ]; + calculateInstanceNormals(attribute: Attribute) { + const {ticks} = this.state; - attribute.value = new Float32Array(flatten(normals)); + const normals = ticks.flatMap(t => { + switch (t.axis) { + case 'x': + return [1, 0, 0]; + case 'z': + // Flip y and z + return [0, 1, 0]; + case 'y': + return [0, 0, 1]; + } + }); + attribute.value = new Float32Array(normals); } - calculateInstanceIsTitle(attribute) { + calculateInstanceOffsets(attribute: Attribute) { const {ticks} = this.state; - const isTitle = ticks.map(axisTicks => { - const ticksCount = axisTicks.length - 1; - return axisTicks.map((t, i) => (i < ticksCount ? 0 : 1)); + const offsets = ticks.flatMap(t => { + return t.value === 'title' ? 2 : 0.5; }); - - attribute.value = new Float32Array(flatten(isTitle)); + attribute.value = new Float32Array(offsets); } - renderLabelTexture(ticks): LabelTexture | null { + renderLabelTexture(ticks: Tick[]): LabelTexture | null { if (this.state.labelTexture) { this.state.labelTexture.labelTexture.destroy(); } + const labelsbyAxis: [x: string[], z: string[], y: string[]] = [[], [], []]; + for (const t of ticks) { + switch (t.axis) { + case 'x': + labelsbyAxis[0].push(t.text); + break; + case 'z': + labelsbyAxis[1].push(t.text); + break; + case 'y': + labelsbyAxis[2].push(t.text); + break; + } + } + // attach a 2d texture of all the label texts - const textureInfo = textMatrixToTexture(this.context.gl, ticks, DEFAULT_FONT_SIZE); + const textureInfo = textMatrixToTexture(this.context.device, labelsbyAxis); if (textureInfo) { // success - const {columnWidths, texture} = textureInfo; + const {rowHeight, columnWidths, texture} = textureInfo; return { - labelHeight: DEFAULT_FONT_SIZE, + labelHeight: rowHeight, labelWidths: columnWidths, - labelTextureDim: [texture.width, texture.height], labelTexture: texture }; } @@ -360,37 +322,31 @@ export default class AxesLayer extends } } -/* Utils */ -function flatten(arrayOfArrays) { - const flatArray = arrayOfArrays.reduce((acc, arr) => acc.concat(arr), []); - if (Array.isArray(flatArray[0])) { - return flatten(flatArray); +function getRange(axis: Axis): [number, number] { + const {min, max, scale} = axis; + if (scale) { + return [scale(min), scale(max)]; } - return flatArray; + return [min, max]; } -function getTicks(props: AxesLayerProps & {axis: Axis}): Tick[] { - const {axis} = props; - let ticks = props[`${axis}Ticks`] as number | number[]; - const scale = props[`${axis}Scale`]; - const tickFormat = props[`${axis}TickFormat`]; - - if (!Array.isArray(ticks)) { - ticks = scale.ticks(ticks) as number[]; - } - - const titleTick = { - value: props[`${axis}Title`], - position: (scale.range()[0] + scale.range()[1]) / 2, - text: props[`${axis}Title`] - }; +function getTicks(axis: Axis, tickFormat: TickFormat): Tick[] { + const {min, max} = axis; + const ticks = axis.ticks ?? [min, max]; + const scale = axis.scale ?? (x => x); return [ - ...ticks.map(t => ({ + ...ticks.map((t, i) => ({ + axis: axis.name, value: String(t), - position: scale(t), + position: [scale(t), i], text: tickFormat(t, axis) - })), - titleTick + }) as Tick), + { + axis: axis.name, + value: 'title', + position: [(scale(min) + scale(max)) / 2, ticks.length], + text: axis.title ?? axis.name + } ]; } diff --git a/examples/website/plot/plot-layer/axes-fragment.glsl.js b/examples/website/plot/plot-layer/grid-fragment.glsl.ts similarity index 96% rename from examples/website/plot/plot-layer/axes-fragment.glsl.js rename to examples/website/plot/plot-layer/grid-fragment.glsl.ts index 7a83ce8ce45..2ce46f80a47 100644 --- a/examples/website/plot/plot-layer/axes-fragment.glsl.js +++ b/examples/website/plot/plot-layer/grid-fragment.glsl.ts @@ -20,7 +20,7 @@ export default `\ #version 300 es -#define SHADER_NAME graph-layer-fragment-shader +#define SHADER_NAME axes-layer-grid-fragment-shader precision highp float; diff --git a/examples/website/plot/plot-layer/grid-vertex.glsl.js b/examples/website/plot/plot-layer/grid-vertex.glsl.ts similarity index 98% rename from examples/website/plot/plot-layer/grid-vertex.glsl.js rename to examples/website/plot/plot-layer/grid-vertex.glsl.ts index 6a6964b565a..0157906e837 100644 --- a/examples/website/plot/plot-layer/grid-vertex.glsl.js +++ b/examples/website/plot/plot-layer/grid-vertex.glsl.ts @@ -20,7 +20,7 @@ export default `\ #version 300 es -#define SHADER_NAME graph-layer-axis-vertex-shader +#define SHADER_NAME axes-layer-grid-vertex-shader in vec3 positions; in vec3 normals; diff --git a/examples/website/plot/plot-layer/index.js b/examples/website/plot/plot-layer/index.ts similarity index 56% rename from examples/website/plot/plot-layer/index.js rename to examples/website/plot/plot-layer/index.ts index bc0d4939803..55a53165279 100644 --- a/examples/website/plot/plot-layer/index.js +++ b/examples/website/plot/plot-layer/index.ts @@ -1,3 +1,6 @@ export {default as AxesLayer} from './axes-layer'; export {default as SurfaceLayer} from './surface-layer'; export {default} from './plot-layer'; + +export type {PlotLayerProps} from './plot-layer'; +export type {Axis, Axes, PlotLayerPickingInfo} from './types'; \ No newline at end of file diff --git a/examples/website/plot/plot-layer/label-fragment.glsl.js b/examples/website/plot/plot-layer/label-fragment.glsl.ts similarity index 96% rename from examples/website/plot/plot-layer/label-fragment.glsl.js rename to examples/website/plot/plot-layer/label-fragment.glsl.ts index 608ad8cf4e2..d583a3055e8 100644 --- a/examples/website/plot/plot-layer/label-fragment.glsl.js +++ b/examples/website/plot/plot-layer/label-fragment.glsl.ts @@ -20,7 +20,7 @@ export default `\ #version 300 es -#define SHADER_NAME graph-layer-fragment-shader +#define SHADER_NAME axes-layer-label-fragment-shader precision highp float; diff --git a/examples/website/plot/plot-layer/label-vertex.glsl.js b/examples/website/plot/plot-layer/label-vertex.glsl.ts similarity index 89% rename from examples/website/plot/plot-layer/label-vertex.glsl.js rename to examples/website/plot/plot-layer/label-vertex.glsl.ts index a91e338c823..b5b8610f3b7 100644 --- a/examples/website/plot/plot-layer/label-vertex.glsl.js +++ b/examples/website/plot/plot-layer/label-vertex.glsl.ts @@ -20,14 +20,14 @@ export default `\ #version 300 es -#define SHADER_NAME graph-layer-axis-vertex-shader +#define SHADER_NAME axes-layer-label-vertex-shader in vec3 positions; in vec3 normals; in vec2 texCoords; in vec2 instancePositions; in vec3 instanceNormals; -in float instanceIsTitle; +in float instanceOffsets; uniform vec3 gridDims; uniform vec3 gridCenter; @@ -35,14 +35,11 @@ uniform float gridOffset; uniform vec3 labelWidths; uniform float fontSize; uniform float labelHeight; -uniform vec2 labelTextureDim; +uniform sampler2D labelTexture; out vec2 vTexCoords; out float shouldDiscard; -const float LABEL_OFFSET = 0.02; -const float TITLE_OFFSET = 0.06; - float sum2(vec2 v) { return v.x + v.y; } @@ -93,21 +90,20 @@ void main(void) { sum3(vec3(0.0, labelWidths.x, sum2(labelWidths.xy)) * instanceNormals), instancePositions.y * labelHeight ); - vec2 textureSize = vec2(sum3(labelWidths * instanceNormals), labelHeight); - - vTexCoords = (textureOrigin + textureSize * texCoords) / labelTextureDim; + vec2 labelSize = vec2(sum3(labelWidths * instanceNormals), labelHeight); + vTexCoords = (textureOrigin + labelSize * texCoords) / vec2(textureSize(labelTexture, 0)); vec3 position_modelspace = vec3(instancePositions.x) * instanceNormals + gridVertexOffset * gridDims / 2.0 + gridCenter * abs(gridVertexOffset); // apply offsets position_modelspace += gridOffset * gridLineNormal; - position_modelspace += (LABEL_OFFSET + (instanceIsTitle * TITLE_OFFSET)) * gridVertexOffset; + position_modelspace += project_pixel_size(fontSize * instanceOffsets) * gridVertexOffset; vec3 position_commonspace = project_position(position_modelspace); vec4 position_clipspace = project_common_position_to_clipspace(vec4(position_commonspace, 1.0)); - vec2 labelVertexOffset = vec2(texCoords.x - 0.5, 0.5 - texCoords.y) * textureSize; + vec2 labelVertexOffset = vec2(texCoords.x - 0.5, 0.5 - texCoords.y) * labelSize; // project to clipspace labelVertexOffset = project_pixel_size_to_clipspace(labelVertexOffset).xy; // scale label to be constant size in pixels diff --git a/examples/website/plot/plot-layer/plot-layer.ts b/examples/website/plot/plot-layer/plot-layer.ts index b40556a6417..49259775b55 100644 --- a/examples/website/plot/plot-layer/plot-layer.ts +++ b/examples/website/plot/plot-layer/plot-layer.ts @@ -1,64 +1,52 @@ import { - Accessor, - AccessorContext, - AccessorFunction, Color, CompositeLayer, CompositeLayerProps, - COORDINATE_SYSTEM, DefaultProps, + UpdateParameters, + GetPickingInfoParams } from '@deck.gl/core'; -import {ScaleLinear, scaleLinear} from 'd3-scale'; import AxesLayer from './axes-layer'; import SurfaceLayer from './surface-layer'; -import {Range, TickFormat, Vec3} from './types'; - -const DEFAULT_GET_SCALE = {type: 'function', value: () => scaleLinear()} as const; -const DEFAULT_TICK_FORMAT = {type: 'function', value: (x: number) => x.toFixed(2)} as const; -const DEFAULT_TICK_COUNT = 6; -const DEFAULT_COLOR: Color = [0, 0, 0, 255]; +import {Axes, TickFormat, Vec3, PlotLayerPickingInfo} from './types'; /** All props supported by PlotLayer. */ -export type PlotLayerProps = _PlotLayerProps & CompositeLayerProps; +export type PlotLayerProps = _PlotLayerProps & CompositeLayerProps; -type _PlotLayerProps = { +type _PlotLayerProps= { // SurfaceLayer props - getPosition: AccessorFunction; - getColor: AccessorFunction; - getXScale: AccessorFunction>; - getYScale: AccessorFunction>; - getZScale: AccessorFunction>; - uCount: number; - vCount: number; - lightStrength: number; + /** Function called to get surface coordinate [x, y, z] from [u, v] */ + getPosition: (uv: [u: number, v: number]) => [x: number, y: number, z: number]; + /** Function called to get surface color [r, g, b, a] from [x, y, z] */ + getColor?: Color | ((position: [x: number, y: number, z: number]) => Color); + /** Callback to supply additional axis settings */ + onAxesChange?: (axes: Axes) => void; + /** Number of U samples between [0, 1] */ + uCount?: number; + /** Number of V samples between [0, 1] */ + vCount?: number; + /** Front light strength */ + lightStrength?: number; // AxesLayer props + /** Whether to draw axes */ drawAxes?: boolean; + /** Label size */ fontSize?: number; - xScale?: ScaleLinear; - yScale?: ScaleLinear; - zScale?: ScaleLinear; - xTicks: number; - yTicks: number; - zTicks: number; - xTickFormat: TickFormat; - yTickFormat: TickFormat; - zTickFormat: TickFormat; - axesPadding: 0; - axesColor: Color; - xTitle: string; - yTitle: string; - zTitle: string; + /** Function called to convert tick value to string */ + tickFormat?: TickFormat; + /** Padding between the axes and the graph surface */ + axesPadding?: number; + /** Color of the axes */ + axesColor?: Color; }; const defaultProps: DefaultProps = { // SurfaceLayer props - getPosition: {type: 'accessor', value: ([u, v]) => [0, 0, 0]}, - getColor: {type: 'accessor', value: ([x, y, z]) => DEFAULT_COLOR}, - getXScale: DEFAULT_GET_SCALE, - getYScale: DEFAULT_GET_SCALE, - getZScale: DEFAULT_GET_SCALE, + getPosition: {type: 'function', value: (uv) => [0, 0, 0]}, + getColor: {type: 'accessor', value: [128, 128, 128, 255]}, + onAxesChange: {type: 'function', value: axes => axes}, uCount: 100, vCount: 100, lightStrength: 0.1, @@ -66,66 +54,42 @@ const defaultProps: DefaultProps = { // AxesLayer props drawAxes: true, fontSize: 12, - xTicks: DEFAULT_TICK_COUNT, - yTicks: DEFAULT_TICK_COUNT, - zTicks: DEFAULT_TICK_COUNT, - xTickFormat: DEFAULT_TICK_FORMAT as unknown as TickFormat, - yTickFormat: DEFAULT_TICK_FORMAT as unknown as TickFormat, - zTickFormat: DEFAULT_TICK_FORMAT as unknown as TickFormat, - xTitle: 'x', - yTitle: 'y', - zTitle: 'z', + tickFormat: {type: 'function', value: (x: number) => x.toFixed(2)}, axesPadding: 0, - axesColor: [0, 0, 0, 255], - coordinateSystem: COORDINATE_SYSTEM.CARTESIAN + axesColor: {type: 'color', value: [0, 0, 0, 255]} }; -/* - * @classdesc +/** * A layer that plots a surface based on a z=f(x,y) equation. - * - * @class - * @param {Object} [props] - * @param {Function} [props.getPosition] - method called to get [x, y, z] from (u,v) values - * @param {Function} [props.getColor] - method called to get color from (x,y,z) - returns [r,g,b,a]. - * @param {Integer} [props.uCount] - number of samples within x range - * @param {Integer} [props.vCount] - number of samples within y range - * @param {Number} [props.lightStrength] - front light strength - * @param {Boolean} [props.drawAxes] - whether to draw axes - - * @param {Function} [props.getXScale] - returns a d3 scale from (params = {min, max}) - * @param {Function} [props.getYScale] - returns a d3 scale from (params = {min, max}) - * @param {Function} [props.getZScale] - returns a d3 scale from (params = {min, max}) - * @param {Number | [Number]} [props.xTicks] - either tick counts or an array of tick values - * @param {Number | [Number]} [props.yTicks] - either tick counts or an array of tick values - * @param {Number | [Number]} [props.zTicks] - either tick counts or an array of tick values - * @param {Function} [props.xTickFormat] - returns a string from value - * @param {Function} [props.yTickFormat] - returns a string from value - * @param {Function} [props.zTickFormat] - returns a string from value - * @param {String} [props.xTitle] - x axis title - * @param {String} [props.yTitle] - y axis title - * @param {String} [props.zTitle] - z axis title - - * @param {Number} [props.axesPadding] - amount to set back grids from the plot, - relative to the size of the bounding box - * @param {Number} [props.fontSize] - size of the labels - * @param {Array} [props.axesColor] - color of the gridlines, in [r,g,b,a] */ -export default class PlotLayer extends CompositeLayer< - ExtraPropsT & Required<_PlotLayerProps> -> { +export default class PlotLayer extends CompositeLayer> { static layerName = 'PlotLayer'; static defaultProps = defaultProps; state!: CompositeLayer['state'] & { - xScale: ScaleLinear; - yScale: ScaleLinear; - zScale: ScaleLinear; + axes: Axes; + samples: Vec3[]; }; - updateState() { - const {uCount, vCount, getPosition, getXScale, getYScale, getZScale} = this.props; + getPickingInfo(opts: GetPickingInfoParams) { + const info = opts.info as PlotLayerPickingInfo; + if (info.uv) { + info.sample = this.props.getPosition(info.uv); + } + return info; + } + + updateState({props, oldProps, changeFlags}: UpdateParameters) { + if (props.uCount !== oldProps.uCount || + props.vCount !== oldProps.vCount || + (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getPosition)) { + this.getSamples(); + } + } + + getSamples() { + const {uCount, vCount, getPosition, onAxesChange} = this.props; + const samples: Vec3[] = new Array(uCount * vCount); // calculate z range let xMin = Infinity; @@ -135,12 +99,15 @@ export default class PlotLayer); + const p = getPosition([u, v]); + samples[i++] = p; + const [x, y, z] = p; if (isFinite(x)) { xMin = Math.min(xMin, x); xMax = Math.max(xMax, x); @@ -156,31 +123,25 @@ export default class PlotLayer); - const yScale = getYScale({min: yMin, max: yMax}, {} as AccessorContext); - const zScale = getZScale({min: zMin, max: zMax}, {} as AccessorContext); + const axes: Axes = { + x: {name: 'x', min: xMin, max: xMax}, + y: {name: 'y', min: yMin, max: yMax}, + z: {name: 'z', min: zMin, max: zMax} + }; + onAxesChange(axes); - this.setState({xScale, yScale, zScale}); + this.setState({axes, samples}); } renderLayers() { - const {xScale, yScale, zScale} = this.state; + const {axes: {x: xAxis, y: yAxis, z: zAxis}, samples} = this.state; const { - getPosition, getColor, uCount, vCount, lightStrength, fontSize, - xTicks, - yTicks, - zTicks, - xTickFormat, - yTickFormat, - zTickFormat, - xTitle, - yTitle, - zTitle, + tickFormat, axesPadding, axesColor, drawAxes, @@ -188,15 +149,16 @@ export default class PlotLayer( { - getPosition: getPosition as AccessorFunction, - getColor: getColor as Accessor, + data: samples, + getPosition: p => p, + getColor, uCount, vCount, - xScale, - yScale, - zScale, + xAxis, + yAxis, + zAxis, lightStrength }, this.getSubLayerProps({ @@ -206,19 +168,11 @@ export default class PlotLayer = { data: [], - getPosition: () => [0, 0, 0], - getColor: () => DEFAULT_COLOR, - xScale: undefined, - yScale: undefined, - zScale: undefined, + getColor: {type: 'accessor', value: [128, 128, 128, 255]}, uCount: 100, vCount: 100, lightStrength: 0.1 }; /** All props supported by SurfaceLayer. */ -export type SurfaceLayerProps = _SurfaceLayerProps & LayerProps; +export type SurfaceLayerProps = _SurfaceLayerProps & LayerProps; -type _SurfaceLayerProps = { - data: LayerDataSource; - getPosition?: AccessorFunction; +type _SurfaceLayerProps = { + data: DataT[]; + getPosition: Accessor; getColor?: Accessor; - xScale?: ScaleLinear; - yScale?: ScaleLinear; - zScale?: ScaleLinear; + xAxis: Axis; + yAxis: Axis; + zAxis: Axis; uCount?: number; vCount?: number; lightStrength?: number; }; -/* - * @classdesc +/** * A layer that plots a surface based on a z=f(x,y) equation. - * - * @class - * @param {Object} [props] - * @param {Function} [props.getPosition] - method called to get [x, y, z] from (u,v) values - * @param {Function} [props.getColor] - method called to get color from (x,y,z) - returns [r,g,b,a]. - * @param {d3.scale} [props.xScale] - a d3 scale for the x axis - * @param {d3.scale} [props.yScale] - a d3 scale for the y axis - * @param {d3.scale} [props.zScale] - a d3 scale for the z axis - * @param {Integer} [props.uCount] - number of samples within x range - * @param {Integer} [props.vCount] - number of samples within y range - * @param {Number} [props.lightStrength] - front light strength */ -export default class SurfaceLayer< - DataT extends Vec3 = Vec3, - ExtraPropsT extends {} = {} -> extends Layer>> { +export default class SurfaceLayer extends Layer>> { static defaultProps = defaultProps; static layerName: string = 'SurfaceLayer'; - state!: Layer['state'] & { - model?: Model; + state!: { + model: Model; vertexCount: number; }; initializeState() { - const noAlloc = true; - + const attributeManager = this.getAttributeManager()!; + attributeManager.remove(['instancePickingColors']); /* eslint-disable max-len */ - this.getAttributeManager()!.add({ - indices: {size: 1, isIndexed: true, update: this.calculateIndices, noAlloc}, - positions: {size: 4, accessor: 'getPosition', update: this.calculatePositions, noAlloc}, + attributeManager.add({ + indices: { + size: 1, + isIndexed: true, + update: this.calculateIndices, + noAlloc: true + }, + positions: { + size: 3, + type: 'float64', + accessor: 'getPosition', + transform: this.getPosition + }, colors: { size: 4, - accessor: ['getPosition', 'getColor'], + accessor: 'getColor', type: 'uint8', - update: this.calculateColors, - noAlloc + defaultValue: [0, 0, 0, 255] }, - pickingColors: {size: 3, type: 'uint8', update: this.calculatePickingColors, noAlloc} + pickingColors: { + size: 4, + type: 'uint8', + accessor: (_, {index}) => index, + transform: this.getPickingColor + } }); /* eslint-enable max-len */ this.state.model = this.getModel(); } - updateState({oldProps, props, changeFlags}) { + updateState(params: UpdateParameters) { + super.updateState(params); + const {oldProps, props, changeFlags} = params; + if (changeFlags.propsChanged) { const {uCount, vCount} = props; if (oldProps.uCount !== uCount || oldProps.vCount !== vCount) { this.state.vertexCount = uCount * vCount; - this.getAttributeManager()!.invalidateAll(); + if (props.data.length < uCount * vCount) { + throw new Error('SurfaceLayer: insufficient data'); + } + this.getAttributeManager()!.invalidate('indices'); } } } @@ -110,6 +107,7 @@ export default class SurfaceLayer< id: `${this.props.id}-surface`, vs: surfaceVertex, fs: fragmentShader, + disableWarnings: true, modules: [picking], topology: 'triangle-list', bufferLayout: this.getAttributeManager()!.getBufferLayouts(), @@ -119,9 +117,13 @@ export default class SurfaceLayer< draw({uniforms}) { const {lightStrength} = this.props; + const {model} = this.state; - this.state.model!.setUniforms(Object.assign({}, uniforms, {lightStrength})); - this.state.model!.draw(this.context.renderPass); + // This is a non-instanced model + model.setInstanceCount(0); + model.setUniforms(uniforms); + model.setUniforms({lightStrength}); + model.draw(this.context.renderPass); } /* @@ -133,13 +135,19 @@ export default class SurfaceLayer< * 0--------> 1 * x */ - encodePickingColor(i: number, target: number[] = []) { - const {uCount, vCount} = this.props; - - const xIndex = i % uCount; - const yIndex = (i - xIndex) / uCount; - - return [(xIndex / (uCount - 1)) * 255, (yIndex / (vCount - 1)) * 255, 1]; + getPickingColor(index: number) { + const {data, uCount, vCount} = this.props; + + const xIndex = index % uCount; + const yIndex = (index - xIndex) / uCount; + const p = data[index]; + + return [ + (xIndex / (uCount - 1)) * 255, + (yIndex / (vCount - 1)) * 255, + 1, + isFinite(p[0]) && isFinite(p[1]) && isFinite(p[2]) ? 0 : 1 + ]; } decodePickingColor([r, g, b]: Color): number { @@ -149,20 +157,28 @@ export default class SurfaceLayer< return r | (g << 8); } - getPickingInfo(opts) { - const {info} = opts; + getPosition([x, y, z]: Vec3) { + const {xAxis, yAxis, zAxis} = this.props; + + return [ + xAxis.scale?.(x) ?? x, + // swap z and y: y is up in the default viewport + zAxis.scale?.(z) ?? z, + yAxis.scale?.(y) ?? y + ]; + } + + getPickingInfo(opts: GetPickingInfoParams) { + const info = opts.info as PlotLayerPickingInfo; if (info && info.index !== -1) { - const {getPosition} = this.props; - const u = (info.index & 255) / 255; - const v = (info.index >> 8) / 255; - info.sample = getPosition([u, v, 0] as DataT, {} as AccessorContext); + info.uv = [(info.index & 255) / 255, (info.index >> 8) / 255]; } return info; } - calculateIndices(attribute) { + calculateIndices(attribute: Attribute) { const {uCount, vCount} = this.props; // # of squares = (nx - 1) * (ny - 1) // # of triangles = squares * 2 @@ -196,76 +212,7 @@ export default class SurfaceLayer< } attribute.value = indices; - this.state.model!.setVertexCount(indicesCount); - } - - // the fourth component is a flag for invalid z (NaN or Infinity) - /* eslint-disable max-statements */ - calculatePositions(attribute) { - const {vertexCount} = this.state; - const {uCount, vCount, getPosition, xScale, yScale, zScale} = this.props; - - const value = new Float32Array(vertexCount * attribute.size); - - let i = 0; - for (let vIndex = 0; vIndex < vCount; vIndex++) { - for (let uIndex = 0; uIndex < uCount; uIndex++) { - const u = uIndex / (uCount - 1); - const v = vIndex / (vCount - 1); - const [x, y, z] = getPosition([u, v, 0] as DataT, {} as AccessorContext); - - const isXFinite = isFinite(x); - const isYFinite = isFinite(y); - const isZFinite = isFinite(z); - - // swap z and y: y is up in the default viewport - value[i++] = isXFinite ? xScale(x) : xScale.range()[0]; - value[i++] = isZFinite ? zScale(z) : zScale.range()[0]; - value[i++] = isYFinite ? yScale(y) : yScale.range()[0]; - value[i++] = isXFinite && isYFinite && isZFinite ? 0 : 1; - } - } - - attribute.value = value; + this.state.model.setVertexCount(indicesCount); } - /* eslint-enable max-statements */ - - calculateColors(attribute) { - const {vertexCount} = this.state; - - // reuse the calculated [x, y, z] in positions - const positions = this.getAttributeManager()!.attributes.positions.value!; - const value = new Uint8ClampedArray(vertexCount! * attribute.size); - - // Support constant colors - const getColor = - typeof this.props.getColor === 'function' ? this.props.getColor : () => this.props.getColor; - for (let i = 0; i < vertexCount; i++) { - const index = i * 4; - const position = [positions[index], positions[index + 2], positions[index + 1]]; - const color = getColor(position as DataT, {} as AccessorContext); - value[i * 4] = color[0]; - value[i * 4 + 1] = color[1]; - value[i * 4 + 2] = color[2]; - value[i * 4 + 3] = isNaN(color[3]) ? 255 : color[3]; - } - - attribute.value = value; - } - - calculatePickingColors(attribute) { - const {vertexCount} = this.state; - - const value = new Uint8ClampedArray(vertexCount * attribute.size); - - for (let i = 0; i < vertexCount; i++) { - const pickingColor = this.encodePickingColor(i); - value[i * 3] = pickingColor[0]; - value[i * 3 + 1] = pickingColor[1]; - value[i * 3 + 2] = pickingColor[2]; - } - - attribute.value = value; - } } diff --git a/examples/website/plot/plot-layer/surface-vertex.glsl.js b/examples/website/plot/plot-layer/surface-vertex.glsl.ts similarity index 86% rename from examples/website/plot/plot-layer/surface-vertex.glsl.js rename to examples/website/plot/plot-layer/surface-vertex.glsl.ts index 1643ee9ee8e..c7054b3916c 100644 --- a/examples/website/plot/plot-layer/surface-vertex.glsl.js +++ b/examples/website/plot/plot-layer/surface-vertex.glsl.ts @@ -20,11 +20,12 @@ export default `\ #version 300 es -#define SHADER_NAME graph-layer-vertex-shader +#define SHADER_NAME surface-layer-vertex-shader -in vec4 positions; +in vec3 positions; +in vec3 positions64Low; in vec4 colors; -in vec3 pickingColors; +in vec4 pickingColors; uniform float lightStrength; uniform float opacity; @@ -33,9 +34,7 @@ out vec4 vColor; out float shouldDiscard; void main(void) { - - // fit into a unit cube that centers at [0, 0, 0] - vec3 position_commonspace = project_position(positions.xyz); + vec3 position_commonspace = project_position(positions, positions64Low); gl_Position = project_common_position_to_clipspace(vec4(position_commonspace, 1.0)); // cheap way to produce believable front-lit effect. @@ -46,8 +45,8 @@ void main(void) { vColor = vec4(colors.rgb * fadeFactor, colors.a * opacity) / 255.0;; - picking_setPickingColor(pickingColors); + picking_setPickingColor(pickingColors.xyz); - shouldDiscard = positions.w; + shouldDiscard = pickingColors.a; } `; diff --git a/examples/website/plot/plot-layer/types.ts b/examples/website/plot/plot-layer/types.ts index a10be35ddfb..083cfdf81c9 100644 --- a/examples/website/plot/plot-layer/types.ts +++ b/examples/website/plot/plot-layer/types.ts @@ -1,14 +1,34 @@ -export type Vec2 = [number, number]; -export type Vec3 = [number, number, number]; +import type {PickingInfo} from "@deck.gl/core"; -export type Range = {min: number, max: number}; +export type Vec3 = [x: number, y: number, z: number]; -export type Axis = 'x' | 'y' | 'z'; +export type Axis= { + name: Name, + /** Minimum value to show on this axis */ + min: number; + /** Maximum value to show on this axis */ + max: number; + /** Label for the axis, default to the value of `name` + */ + title?: string; + /** If specified, will be used to remap input value to model space */ + scale?: (x: number) => number; + /** A list of input values at which to draw grid lines and labels + * @default [min, max] + */ + ticks?: number[]; +}; -export type Tick = { - value: string, - position: number, - text: string, +export type Axes = { + x: Axis<'x'>, + y: Axis<'y'>, + z: Axis<'z'> }; -export type TickFormat = (t: DataT, axis: Axis) => string; +export type TickFormat = (t: number, axis: Axis) => string; + +export type PlotLayerPickingInfo = PickingInfo; diff --git a/examples/website/plot/plot-layer/utils.js b/examples/website/plot/plot-layer/utils.ts similarity index 77% rename from examples/website/plot/plot-layer/utils.js rename to examples/website/plot/plot-layer/utils.ts index c214b727211..648b305b074 100644 --- a/examples/website/plot/plot-layer/utils.js +++ b/examples/website/plot/plot-layer/utils.ts @@ -1,4 +1,5 @@ /* global document */ +import type {Device} from "@luma.gl/core"; // helper for textMatrixToTexture function setTextStyle(ctx, fontSize) { @@ -15,15 +16,15 @@ function setTextStyle(ctx, fontSize) { * @param {Number} fontSize: size to render with * @returns {object} {texture, columnWidths} */ -export function textMatrixToTexture(glContext, data, fontSize = 48) { +export function textMatrixToTexture(device: Device, data: string[][], fontSize: number = 48) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); setTextStyle(ctx, fontSize); // measure texts const columnWidths = data.map(column => { - return column.reduce((acc, obj) => { - const w = ctx.measureText(obj.text).width; + return column.reduce((acc, text) => { + const w = ctx.measureText(text).width; return Math.max(acc, Math.ceil(w)); }, 0); }); @@ -56,17 +57,21 @@ export function textMatrixToTexture(glContext, data, fontSize = 48) { let x = 0; data.forEach((column, colIndex) => { x += columnWidths[colIndex] / 2; - column.forEach((obj, i) => { - ctx.fillText(obj.text, x, i * fontSize); + column.forEach((text, i) => { + ctx.fillText(text, x, i * fontSize); }); x += columnWidths[colIndex] / 2; }); return { + rowHeight: fontSize, columnWidths, - texture: glContext.device.createTexture({ - pixels: canvas, - magFilter: 'linear' + texture: device.createTexture({ + data: canvas as any, + mipmaps: true, + sampler: { + magFilter: 'linear' + } }) }; } diff --git a/examples/website/plot/tsconfig.json b/examples/website/plot/tsconfig.json new file mode 100644 index 00000000000..9b3c020493c --- /dev/null +++ b/examples/website/plot/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/examples/website/radio/app.jsx b/examples/website/radio/app.tsx similarity index 56% rename from examples/website/radio/app.jsx rename to examples/website/radio/app.tsx index 29963827ac6..eacb26a5c00 100644 --- a/examples/website/radio/app.jsx +++ b/examples/website/radio/app.tsx @@ -6,15 +6,14 @@ import {MapView, WebMercatorViewport, FlyToInterpolator} from '@deck.gl/core'; import {ScatterplotLayer, PathLayer} from '@deck.gl/layers'; import {MVTLayer, H3HexagonLayer} from '@deck.gl/geo-layers'; import DeckGL from '@deck.gl/react'; - import {load} from '@loaders.gl/core'; import {CSVLoader} from '@loaders.gl/csv'; - import {scaleSqrt, scaleLinear} from 'd3-scale'; -import {GeoJsonLayer} from '@deck.gl/layers'; - import SearchBar from './search-bar'; +import type {Color, ViewStateChangeParameters, MapViewState, PickingInfo, FilterContext} from '@deck.gl/core'; +import type {Feature, Geometry} from 'geojson'; + // Data source const DATA_URL_BASE = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/radio'; const DATA_URL = { @@ -33,7 +32,7 @@ const minimapView = new MapView({ clear: true }); -const minimapBackgroundStyle = { +const minimapBackgroundStyle: React.CSSProperties = { position: 'absolute', zIndex: -1, width: '100%', @@ -42,7 +41,7 @@ const minimapBackgroundStyle = { boxShadow: '0 0 8px 2px rgba(0,0,0,0.15)' }; -const INITIAL_VIEW_STATE = { +const INITIAL_VIEW_STATE: {main: MapViewState; minimap: MapViewState} = { main: { longitude: -100, latitude: 40, @@ -56,38 +55,61 @@ const INITIAL_VIEW_STATE = { } }; -const coverageColorScale = scaleSqrt() +const coverageColorScale = scaleSqrt() .domain([1, 10, 100]) .range([ [237, 248, 177], [127, 205, 187], [44, 127, 184] ]); -const fmColorScale = scaleLinear() +const fmColorScale = scaleLinear() .domain([87, 108]) .range([ [0, 60, 255], [0, 255, 40] ]); -const amColorScale = scaleLinear() +const amColorScale = scaleLinear() .domain([530, 1700]) .range([ [200, 0, 0], [255, 240, 0] ]); -function layerFilter({layer, viewport}) { +export type Station = { + callSign: string; + frequency: number; + type: 'AM' | 'FM'; + city: string; + state: string; + name: string; + longitude: number; + latitude: number; +}; + +type ServiceContour = { + callSign: string; +}; + +function layerFilter({layer, viewport}: FilterContext) { const shouldDrawInMinimap = layer.id.startsWith('coverage') || layer.id.startsWith('viewport-bounds'); if (viewport.id === 'minimap') return shouldDrawInMinimap; return !shouldDrawInMinimap; -} +}; -function getTooltipText(stationMap, {object}) { +function getTooltipText(stationMap: {[callSign: string]: Station}, object: Feature | Station) { if (!object) { return null; } - const {callSign} = object.properties || object; + let callSign: string; + if ('properties' in object) { + callSign = object.properties.callSign; + } else { + callSign = object.callSign; + } + if (!(callSign in stationMap)) { + return null; + } const {name, frequency, type, city, state} = stationMap[callSign]; return `\ ${callSign} @@ -101,22 +123,25 @@ export default function App({ data, mapStyle = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json', showMinimap = true +}: { + data?: Station[]; + mapStyle?: string; + showMinimap?: boolean; }) { const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); const [highlight, setHighlight] = useState(null); - const stationMap = useMemo(() => { - if (data) { - const result = {}; - for (const station of data) { - result[station.callSign] = station; - } - return result; + const stationMap: {[callSign: string]: Station} = useMemo(() => { + if (!data) return null; + + const result = {}; + for (const station of data) { + result[station.callSign] = station; } - return null; + return result; }, [data]); - const onViewStateChange = useCallback(({viewState: newViewState}) => { + const onViewStateChange = useCallback(({viewState: newViewState}: ViewStateChangeParameters) => { setViewState(() => ({ main: newViewState, minimap: { @@ -127,7 +152,7 @@ export default function App({ })); }, []); - const onSelectStation = useCallback(station => { + const onSelectStation = useCallback((station: Station) => { if (station) { setViewState(currentViewState => ({ minimap: currentViewState.minimap, @@ -157,81 +182,74 @@ export default function App({ return [[topLeft, topRight, bottomRight, bottomLeft, topLeft]]; }, [viewState]); - const getTooltip = useCallback(info => getTooltipText(stationMap, info), [stationMap]); - - const layers = data - ? [ - new MVTLayer({ - id: 'service-contours', - data: DATA_URL.CONTOURS, - maxZoom: 8, - onTileError: () => {}, - pickable: true, - autoHighlight: true, - highlightColor: [255, 200, 0, 100], - uniqueIdProperty: 'callSign', - highlightedFeatureId: highlight, - opacity: 0.2, - - renderSubLayers: props => { - return new GeoJsonLayer(props, { - lineWidthMinPixels: 2, - getLineColor: f => { - const {type, frequency} = stationMap[f.properties.callSign]; - return type === 'AM' ? amColorScale(frequency) : fmColorScale(frequency); - }, - getFillColor: [255, 255, 255, 0] - }); - } - }), - - new ScatterplotLayer({ - id: 'stations', - data, - getPosition: d => [d.longitude, d.latitude], - getColor: [40, 40, 40, 128], - getRadius: 20, - radiusMinPixels: 2 - }), - - new H3HexagonLayer({ - id: 'coverage', - data: DATA_URL.COVERAGE, - getHexagon: d => d.hex, - stroked: false, - extruded: false, - getFillColor: d => coverageColorScale(d.count), - - loaders: [CSVLoader] - }), - - new PathLayer({ - id: 'viewport-bounds', - data: viewportBounds, - getPath: d => d, - getColor: [255, 0, 0], - getWidth: 2, - widthUnits: 'pixels' - }) - ] - : []; + const getTooltip = useCallback(({object}: PickingInfo) => getTooltipText(stationMap, object), [stationMap]); + + const layers = [ + data && new MVTLayer({ + id: 'service-contours', + data: DATA_URL.CONTOURS, + maxZoom: 8, + onTileError: () => {}, + pickable: true, + autoHighlight: true, + highlightColor: [255, 200, 0, 100], + uniqueIdProperty: 'callSign', + highlightedFeatureId: highlight, + opacity: 0.2, + lineWidthMinPixels: 2, + getLineColor: f => { + const {type, frequency} = stationMap[f.properties.callSign]; + return type === 'AM' ? amColorScale(frequency) : fmColorScale(frequency); + }, + getFillColor: [255, 255, 255, 0] + }), + + new ScatterplotLayer({ + id: 'stations', + data, + getPosition: d => [d.longitude, d.latitude], + getFillColor: [40, 40, 40, 128], + getRadius: 20, + radiusMinPixels: 2, + }), + + new H3HexagonLayer<{hex: string; count: number;}>({ + id: 'coverage', + data: DATA_URL.COVERAGE, + getHexagon: d => d.hex, + stroked: false, + extruded: false, + getFillColor: d => coverageColorScale(d.count), + + loaders: [CSVLoader] + }), + + new PathLayer({ + id: 'viewport-bounds', + data: viewportBounds, + getPath: d => d, + getColor: [255, 0, 0], + getWidth: 2, + widthUnits: 'pixels' + }) + ]; return ( - + {showMinimap && ( - +
)} @@ -239,11 +257,13 @@ export default function App({ ); } -export function renderToDOM(container) { +export async function renderToDOM(container: HTMLDivElement) { const root = createRoot(container); root.render(); - load(DATA_URL.STATIONS, CSVLoader, {csv: {delimiter: '\t', skipEmptyLines: true}}).then(data => { - root.render(); - }); + const stations = (await load(DATA_URL.STATIONS, CSVLoader, { + csv: {delimitersToGuess: '\t', skipEmptyLines: true} + })).data; + + root.render(); } diff --git a/examples/website/radio/index.html b/examples/website/radio/index.html index 16f037f851e..8f3eea6882d 100644 --- a/examples/website/radio/index.html +++ b/examples/website/radio/index.html @@ -13,7 +13,7 @@
diff --git a/examples/website/radio/package.json b/examples/website/radio/package.json index b00fd0be36a..c0bf4ceeeca 100644 --- a/examples/website/radio/package.json +++ b/examples/website/radio/package.json @@ -12,6 +12,9 @@ "@loaders.gl/csv": "^4.1.4", "@material-ui/core": "^4.11.3", "@material-ui/lab": "^4.0.0-alpha.57", + "@types/d3-scale": "^4.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "d3-scale": "^4.0.0", "deck.gl": "^9.0.0", "maplibre-gl": "^3.0.0", diff --git a/examples/website/radio/search-bar.jsx b/examples/website/radio/search-bar.tsx similarity index 85% rename from examples/website/radio/search-bar.jsx rename to examples/website/radio/search-bar.tsx index 840a5768e2f..d40b71c2afc 100644 --- a/examples/website/radio/search-bar.jsx +++ b/examples/website/radio/search-bar.tsx @@ -2,8 +2,9 @@ import React from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import {makeStyles} from '@material-ui/core/styles'; +import type {Station} from './app'; -const containerStyle = { +const containerStyle: React.CSSProperties = { position: 'absolute', zIndex: 1, bottom: 40, @@ -21,7 +22,7 @@ const useStyles = makeStyles(theme => ({ const MAX_OPTIONS = 30; -function filterOptions(options, {inputValue}) { +function filterOptions(options: Station[], {inputValue}: {inputValue: string}) { if (!inputValue) return []; const result = []; @@ -46,7 +47,10 @@ function filterOptions(options, {inputValue}) { return result; } -function SearchBar(props) { +function SearchBar(props: { + data: Station[]; + onChange: (selectedStation: Station) => void; +}) { const classes = useStyles(); return ( diff --git a/examples/website/radio/tsconfig.json b/examples/website/radio/tsconfig.json new file mode 100644 index 00000000000..9b3c020493c --- /dev/null +++ b/examples/website/radio/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/examples/website/terrain-extension/app.jsx b/examples/website/terrain-extension/app.jsx deleted file mode 100644 index 915d0125889..00000000000 --- a/examples/website/terrain-extension/app.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import {createRoot} from 'react-dom/client'; -import DeckGL from '@deck.gl/react'; -import {TerrainLayer} from '@deck.gl/geo-layers'; -import {GeoJsonLayer} from '@deck.gl/layers'; -import {_TerrainExtension as TerrainExtension} from '@deck.gl/extensions'; - -const DATA_URL = - 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/terrain/tour_de_france_2023_mountain_stages.json'; // eslint-disable-line - -// Set your mapbox token here -const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line - -const INITIAL_VIEW_STATE = { - latitude: 43.09822, - longitude: -0.6194, - zoom: 10, - pitch: 55, - maxZoom: 13.5, - bearing: 0, - maxPitch: 89 -}; - -const TERRAIN_IMAGE = `https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.png?access_token=${MAPBOX_TOKEN}`; -const SURFACE_IMAGE = `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.png?access_token=${MAPBOX_TOKEN}`; - -// https://docs.mapbox.com/help/troubleshooting/access-elevation-data/#mapbox-terrain-rgb -// Note - the elevation rendered by this example is greatly exagerated! -const ELEVATION_DECODER = { - rScaler: 6553.6, - gScaler: 25.6, - bScaler: 0.1, - offset: -10000 -}; - -const COLOR_SCHEME = [255, 255, 0]; // yellow - -function getTooltip({object}) { - return ( - object && { - html: `\ -
${object.properties.tooltip ? 'Stage' : 'Route'}
-
${object.properties.tooltip || object.properties.location}
- ` - } - ); -} - -export default function App({initialViewState = INITIAL_VIEW_STATE}) { - const layers = [ - new TerrainLayer({ - id: 'terrain', - minZoom: 0, - strategy: 'no-overlap', - elevationDecoder: ELEVATION_DECODER, - elevationData: TERRAIN_IMAGE, - texture: SURFACE_IMAGE, - wireframe: false, - color: [255, 255, 255], - operation: 'terrain+draw' - }), - new GeoJsonLayer({ - id: 'gpx-routes', - data: DATA_URL, - getFillColor: COLOR_SCHEME, - getLineColor: COLOR_SCHEME, - getLineWidth: 30, - stroked: false, - lineWidthMinPixels: 2, - pickable: true, - autoHighlight: false, - // text properties - pointType: 'text', - getText: d => d.properties.location, - getPosition: d => d.geometry.coordinates, - getTextColor: d => (d.properties.tooltip ? [255, 255, 255] : COLOR_SCHEME), - getTextSize: d => (d.properties.tooltip ? 16 : 17), - getTextPixelOffset: [0, -45], - getTextAngle: 0, - textOutlineWidth: 5, - textFontSettings: { - sdf: true - }, - getTextAlignmentBaseline: 'top', - extensions: [new TerrainExtension()] - }) - ]; - - return ( - - ); -} - -export function renderToDOM(container) { - createRoot(container).render(); -} diff --git a/examples/website/terrain-extension/app.tsx b/examples/website/terrain-extension/app.tsx new file mode 100644 index 00000000000..16910783fb1 --- /dev/null +++ b/examples/website/terrain-extension/app.tsx @@ -0,0 +1,159 @@ +import React, {useState, useEffect, useMemo} from 'react'; +import {createRoot} from 'react-dom/client'; +import DeckGL from '@deck.gl/react'; +import {TerrainLayer} from '@deck.gl/geo-layers'; +import {GeoJsonLayer, IconLayer, TextLayer} from '@deck.gl/layers'; +import {_TerrainExtension as TerrainExtension} from '@deck.gl/extensions'; + +import type {Color, MapViewState, PickingInfo} from '@deck.gl/core'; +import type {FeatureCollection, Feature, LineString} from 'geojson'; +import type {TerrainLayerProps} from '@deck.gl/geo-layers'; + +const DATA_URL_BASE = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/terrain'; +const DATA_URL = `${DATA_URL_BASE}/tour_de_france_2023.json`; + +// Set your mapbox token here +const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line + +const INITIAL_VIEW_STATE: MapViewState = { + latitude: 43.09822, + longitude: -0.6194, + zoom: 10, + pitch: 55, + maxZoom: 13.5, + bearing: 0, + maxPitch: 89 +}; + +const TERRAIN_IMAGE = `https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.png?access_token=${MAPBOX_TOKEN}`; +const SURFACE_IMAGE = `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.png?access_token=${MAPBOX_TOKEN}`; + +// https://docs.mapbox.com/help/troubleshooting/access-elevation-data/#mapbox-terrain-rgb +// Note - the elevation rendered by this example is greatly exagerated! +const ELEVATION_DECODER: TerrainLayerProps["elevationDecoder"] = { + rScaler: 6553.6, + gScaler: 25.6, + bScaler: 0.1, + offset: -10000 +}; + +const COLOR_SCHEME: Color[] = [ + [166, 206, 227], + [31, 120, 180], + [178, 223, 138], + [51, 160, 44], + [251, 154, 153], + [227, 26, 28], + [253, 191, 111] +]; + +type RouteProperties = { + day: number; + start: string; + finish: string; + km: number; + type: string; +}; +type Route = Feature; + +type Stage = { + day: number; + name: string; + coordinates: [number, number]; + type: 'start' | 'finish'; +}; + +function getTooltip({object}: PickingInfo) { + if (!object) return null; + + const {day, start, finish, km, type} = object.properties; + return `\ + Day ${day}: ${start} - ${finish} + ${km} km (${type}) + `; +} + +export default function App({initialViewState = INITIAL_VIEW_STATE}: { + initialViewState?: MapViewState; +}) { + const [routes, setRoutes] = useState>(); + + useEffect(() => { + fetch(DATA_URL) + .then(resp => resp.json()) + .then(setRoutes); + }, []); + + const stages = useMemo(() => { + return routes?.features.flatMap(f => { + const {coordinates} = f.geometry; + const {day, start, finish} = f.properties; + return [ + {type: 'start', name: start, day, coordinates: coordinates[0]}, + {type: 'finish', name: finish, day, coordinates: coordinates[coordinates.length - 1]} + ] as Stage[]; + }) + }, [routes]) + + const layers = [ + new TerrainLayer({ + id: 'terrain', + minZoom: 0, + strategy: 'no-overlap', + elevationDecoder: ELEVATION_DECODER, + elevationData: TERRAIN_IMAGE, + texture: SURFACE_IMAGE, + wireframe: false, + color: [255, 255, 255], + operation: 'terrain+draw' + }), + new GeoJsonLayer({ + id: 'gpx-routes', + data: routes, + getLineColor: f => COLOR_SCHEME[f.properties.day % COLOR_SCHEME.length], + getLineWidth: 30, + lineWidthMinPixels: 6, + pickable: true, + extensions: [new TerrainExtension()] + }), + new IconLayer({ + id: 'stage-icon', + data: stages, + iconAtlas: `${DATA_URL_BASE}/flag-icons.png`, + iconMapping: `${DATA_URL_BASE}/flag-icons.json`, + getPosition: d => d.coordinates, + getIcon: d => d.type === 'start' ? 'green' : 'checker', + getSize: 32, + extensions: [new TerrainExtension()] + }), + new TextLayer({ + id: 'stage-label', + data: stages, + characterSet: 'auto', + parameters: { + // should not be occluded by terrain + depthCompare: 'always' + }, + getPosition: d => d.coordinates, + getText: d => d.name, + getColor: [255, 255, 255], + getSize: 18, + getAlignmentBaseline: 'top', + extensions: [new TerrainExtension()] + }) + ]; + + return ( + + ); +} + +export function renderToDOM(container: HTMLDivElement) { + createRoot(container).render(); +} diff --git a/examples/website/terrain-extension/index.html b/examples/website/terrain-extension/index.html index 21e34f8ec04..d91d4471749 100644 --- a/examples/website/terrain-extension/index.html +++ b/examples/website/terrain-extension/index.html @@ -12,7 +12,7 @@
diff --git a/examples/website/terrain-extension/package.json b/examples/website/terrain-extension/package.json index 8470dd3c401..862986cb552 100644 --- a/examples/website/terrain-extension/package.json +++ b/examples/website/terrain-extension/package.json @@ -9,6 +9,8 @@ "build": "vite build" }, "dependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "deck.gl": "^9.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/examples/website/terrain-extension/tsconfig.json b/examples/website/terrain-extension/tsconfig.json new file mode 100644 index 00000000000..9b3c020493c --- /dev/null +++ b/examples/website/terrain-extension/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/website/src/examples/home-demo.js b/website/src/examples/home-demo.js index 1a5105fd205..bc597da09de 100644 --- a/website/src/examples/home-demo.js +++ b/website/src/examples/home-demo.js @@ -71,7 +71,7 @@ class HomeDemo extends Component { trips={(data && data[0]) || null} buildings={(data && data[1]) || null} trailLength={180} - animationSpeed={0.5} + animationSpeed={1} theme={this.theme} initialViewState={this.initialViewState} /> diff --git a/website/src/examples/terrain-extension.js b/website/src/examples/terrain-extension.js index e8bf731eb0c..99411869d24 100644 --- a/website/src/examples/terrain-extension.js +++ b/website/src/examples/terrain-extension.js @@ -80,18 +80,19 @@ class TerrainExtensionDemo extends Component { } render() { - const {params, data, ...otherProps} = this.props; - const {location} = params; + const {location} = this.props.params; - const initialViewState = LOCATIONS[location.value]; - initialViewState.pitch = 45; - initialViewState.bearing = 10 * initialViewState.longitude; - initialViewState.transitionDuration = 3000; - initialViewState.transitionInterpolator = new FlyToInterpolator(); + const initialViewState = { + ...LOCATIONS[location.value], + pitch: 45, + maxPitch: 89, + transitionDuration: 3000, + transitionInterpolator: new FlyToInterpolator() + }; return (
- +
); } diff --git a/website/src/examples/trips-layer.js b/website/src/examples/trips-layer.js index 467b72ecf9e..11c4a21a4b5 100644 --- a/website/src/examples/trips-layer.js +++ b/website/src/examples/trips-layer.js @@ -71,7 +71,7 @@ class TripsDemo extends Component { {...otherProps} trips={data && data[0]} buildings={data && data[1]} - animationSpeed={0.5} + animationSpeed={1} trailLength={params.trail.value} /> );