-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: projection test app #9200
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
import * as React from 'react'; | ||
import {createRoot} from 'react-dom/client'; | ||
|
||
import {DeckGL} from '@deck.gl/react'; | ||
import { | ||
View, | ||
PickingInfo, | ||
MapView, | ||
MapViewState, | ||
_GlobeView as GlobeView, | ||
GlobeViewState, | ||
OrthographicView, | ||
OrthographicViewState | ||
} from '@deck.gl/core'; | ||
import {ScatterplotLayer} from '@deck.gl/layers'; | ||
import {makePointGrid, makeLineGrid, Point} from './data'; | ||
import {Map, Source, Layer} from '@vis.gl/react-maplibre'; | ||
// Use dev build for source map and debugging | ||
import maplibregl from 'maplibre-gl/dist/maplibre-gl-dev'; | ||
import {CPULineLayer} from './cpu-line-layer'; | ||
|
||
const VIEWS: Record< | ||
string, | ||
{ | ||
view: View; | ||
viewState: any; | ||
xRange: [number, number]; | ||
yRange: [number, number]; | ||
step: number; | ||
baseMap: 'mercator' | 'globe' | false; | ||
} | ||
> = { | ||
map: { | ||
view: new MapView(), | ||
viewState: { | ||
longitude: 0, | ||
latitude: 0, | ||
zoom: 1 | ||
} satisfies MapViewState, | ||
baseMap: 'mercator', | ||
xRange: [-180, 180], | ||
yRange: [-85, 85], | ||
step: 5 | ||
}, | ||
'map-high-zoom': { | ||
view: new MapView(), | ||
viewState: { | ||
longitude: 24.87, | ||
latitude: 60.175, | ||
zoom: 16 | ||
} satisfies MapViewState, | ||
baseMap: 'mercator', | ||
xRange: [24.86, 24.88], | ||
yRange: [60.17, 60.18], | ||
step: 1 / 3000 | ||
}, | ||
globe: { | ||
view: new GlobeView(), | ||
viewState: { | ||
longitude: 0, | ||
latitude: 0, | ||
zoom: 2 | ||
} satisfies GlobeViewState, | ||
baseMap: 'globe', | ||
xRange: [-180, 180], | ||
yRange: [-85, 85], | ||
step: 5 | ||
}, | ||
orthographic: { | ||
view: new OrthographicView({flipY: false}), | ||
viewState: { | ||
target: [0, 0, 0], | ||
zoom: 0 | ||
} satisfies OrthographicViewState, | ||
baseMap: false, | ||
xRange: [-500, 500], | ||
yRange: [-400, 400], | ||
step: 40 | ||
}, | ||
'orthographic-high-zoom': { | ||
view: new OrthographicView({flipY: false}), | ||
viewState: { | ||
target: [20001, 10001, 0], | ||
zoom: 16 | ||
} satisfies OrthographicViewState, | ||
baseMap: false, | ||
xRange: [20000.99, 20001.01], | ||
yRange: [10000.99, 10001.01], | ||
step: 1 / 3000 | ||
} | ||
} as const; | ||
|
||
function getTooltip({object}: PickingInfo): string | null { | ||
return object ? JSON.stringify(object) : null; | ||
} | ||
|
||
function App() { | ||
const [viewMode, setViewMode] = React.useState<keyof typeof VIEWS>('map'); | ||
|
||
const opts = VIEWS[viewMode]; | ||
|
||
const pointData = React.useMemo(() => makePointGrid(opts), [viewMode]); | ||
const lineData = React.useMemo(() => makeLineGrid(opts), [viewMode]); | ||
const lineDataGeoJson = React.useMemo(() => { | ||
return { | ||
type: 'FeatureCollection', | ||
features: lineData.map(line => ({ | ||
type: 'Feature', | ||
geometry: { | ||
type: 'LineString', | ||
coordinates: line | ||
} | ||
})) | ||
}; | ||
}, [lineData]); | ||
|
||
const layers = [ | ||
// Reference grid when base map is not available (non-geo) | ||
!opts.baseMap && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could it be useful to visually compare the deck lines with the basemap lines by overlapping them and rendering them in different colors? I'm curious about how to perform a projection check |
||
new CPULineLayer<Point[]>({ | ||
id: 'lines', | ||
data: lineData, | ||
getStartPosition: d => d[0], | ||
getEndPosition: d => d[1] | ||
}), | ||
new ScatterplotLayer<Point>({ | ||
id: 'points', | ||
data: pointData, | ||
getPosition: d => d, | ||
getRadius: 5, | ||
getFillColor: d => [ | ||
((d[0] - opts.xRange[0]) / (opts.xRange[1] - opts.xRange[0])) * 255, | ||
((d[1] - opts.yRange[0]) / (opts.yRange[1] - opts.yRange[0])) * 255, | ||
0 | ||
], | ||
opacity: 0.8, | ||
radiusUnits: 'pixels', | ||
radiusMaxPixels: 5, | ||
pickable: true | ||
}) | ||
]; | ||
|
||
return ( | ||
<> | ||
<DeckGL | ||
controller | ||
parameters={{cullMode: 'back'}} | ||
views={opts.view} | ||
initialViewState={opts.viewState} | ||
layers={layers} | ||
getTooltip={getTooltip} | ||
> | ||
{opts.baseMap && ( | ||
<Map | ||
reuseMaps | ||
mapLib={maplibregl} | ||
projection={opts.baseMap} | ||
mapStyle="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json" | ||
> | ||
<Source type="geojson" data={lineDataGeoJson}> | ||
<Layer | ||
type="line" | ||
paint={{ | ||
'line-color': 'black', | ||
'line-width': 1 | ||
}} | ||
/> | ||
</Source> | ||
</Map> | ||
)} | ||
</DeckGL> | ||
<select | ||
value={viewMode} | ||
onChange={evt => setViewMode(evt.target.value as keyof typeof VIEWS)} | ||
> | ||
{Object.keys(VIEWS).map(mode => ( | ||
<option key={mode} value={mode}> | ||
{mode} | ||
</option> | ||
))} | ||
</select> | ||
</> | ||
); | ||
} | ||
|
||
createRoot(document.getElementById('app')!).render(<App />); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import {Layer, LayerContext, Color, UpdateParameters} from '@deck.gl/core'; | ||
|
||
type CPULineLayerProps<DataT = unknown> = { | ||
data: DataT[]; | ||
getStartPosition: (d: DataT) => number[]; | ||
getEndPosition: (d: DataT) => number[]; | ||
width?: number; | ||
color?: string; | ||
}; | ||
|
||
const defaultProps = { | ||
width: 1, | ||
color: 'black' | ||
}; | ||
|
||
export class CPULineLayer<DataT> extends Layer<Required<CPULineLayerProps<DataT>>> { | ||
static layerName = 'CPULineLayer'; | ||
static defaultProps = defaultProps; | ||
|
||
state!: { | ||
container: Element; | ||
}; | ||
|
||
initializeState({device}: LayerContext) { | ||
const canvas = device.canvasContext?.canvas as HTMLCanvasElement; | ||
if (canvas) { | ||
const container = appendSVGElement(canvas.parentElement!, 'svg'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting use of a deck layer. |
||
Object.assign((container as HTMLElement).style, { | ||
position: 'absolute', | ||
top: 0, | ||
left: 0 | ||
}); | ||
this.state = {container}; | ||
} | ||
} | ||
|
||
finalizeState(context: LayerContext): void { | ||
const {container} = this.state; | ||
container.remove(); | ||
} | ||
|
||
updateState(params: UpdateParameters<this>) { | ||
if (params.changeFlags.dataChanged) { | ||
const {data} = this.props; | ||
const {container} = this.state; | ||
container.innerHTML = ''; | ||
const g = appendSVGElement(container, 'g'); | ||
|
||
for (let i = 0; i < data.length; i++) { | ||
appendSVGElement(g, 'path'); | ||
} | ||
} | ||
if (params.changeFlags.propsChanged) { | ||
const {width, color} = this.props; | ||
const {container} = this.state; | ||
const g = container.querySelector('g')!; | ||
setSVGElementAttributes(g, { | ||
fill: 'none', | ||
stroke: color, | ||
strokeWidth: width | ||
}); | ||
} | ||
} | ||
|
||
draw() { | ||
const {data, getStartPosition, getEndPosition} = this.props; | ||
const {container} = this.state; | ||
const {viewport} = this.context; | ||
const lines = container.querySelectorAll('path'); | ||
|
||
setSVGElementAttributes(container, { | ||
width: viewport.width, | ||
height: viewport.height | ||
}); | ||
|
||
for (let i = 0; i < data.length; i++) { | ||
const start = viewport.project(getStartPosition(data[i])); | ||
const end = viewport.project(getEndPosition(data[i])); | ||
|
||
setSVGElementAttributes(lines[i], { | ||
d: `M${start[0]},${start[1]}L${end[0]},${end[1]}` | ||
}); | ||
} | ||
} | ||
} | ||
|
||
function appendSVGElement(parent: Element, elementType: string): Element { | ||
const el = document.createElementNS('http://www.w3.org/2000/svg', elementType); | ||
parent.append(el); | ||
return el; | ||
} | ||
|
||
function setSVGElementAttributes(element: Element, attributes: Record<string, string | number>) { | ||
for (const key in attributes) { | ||
element.setAttribute(key, String(attributes[key])); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
export type Point = [x: number, y: number]; | ||
export type Line = Point[]; | ||
|
||
export function makePointGrid(opts: { | ||
xRange: [min: number, max: number]; | ||
yRange: [min: number, max: number]; | ||
step: number; | ||
}): Point[] { | ||
const {xRange, yRange, step} = opts; | ||
const result: Point[] = []; | ||
for (let y = yRange[0]; y <= yRange[1]; y += step) { | ||
for (let x = xRange[0]; x <= xRange[1]; x += step) { | ||
result.push([x, y]); | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
export function makeLineGrid(opts: { | ||
xRange: [min: number, max: number]; | ||
yRange: [min: number, max: number]; | ||
step: number; | ||
}): Line[] { | ||
const {xRange, yRange, step} = opts; | ||
const result: Line[] = []; | ||
for (let y = yRange[0]; y <= yRange[1]; y += step) { | ||
result.push([ | ||
[xRange[0], y], | ||
[xRange[1], y] | ||
]); | ||
} | ||
for (let x = xRange[0]; x <= xRange[1]; x += step) { | ||
result.push([ | ||
[x, yRange[0]], | ||
[x, yRange[1]] | ||
]); | ||
} | ||
return result; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<!doctype html> | ||
<html lang="en" | ||
xmlns="http://www.w3.org/1999/xhtml" | ||
xmlns:svg="http://www.w3.org/2000/svg"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>deck.gl Example</title> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<style> | ||
body {margin: 0; font-family: sans-serif; width: 100vw; height: 100vh; overflow: hidden;} | ||
select {position: fixed; z-index: 1;} | ||
</style> | ||
</head> | ||
<body> | ||
<div id="app"></div> | ||
</body> | ||
<script type="module" src="app.tsx"></script> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"scripts": { | ||
"start": "vite --open", | ||
"start-local": "vite --config ../vite.config.local.mjs" | ||
}, | ||
"dependencies": { | ||
"deck.gl": "^9.0.0", | ||
"maplibre-gl": "5.0.0-pre.1", | ||
"react": "^18.0.0", | ||
"react-dom": "^18.0.0", | ||
"@vis.gl/react-maplibre": "^1.0.0-alpha.4" | ||
}, | ||
"devDependencies": { | ||
"vite": "^4.0.0" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to include a finer grid around zoom 12 to see how much we deviate from the adaptive projection in maplibre