Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions test/apps/projection/app.tsx
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]);
Copy link
Collaborator

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

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 &&
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 />);
97 changes: 97 additions & 0 deletions test/apps/projection/cpu-line-layer.ts
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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting use of a deck layer.
Is it correct to say this renders a 2D grid with SVG over the canvas? I'm assuming this only works since pitch is 0?

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]));
}
}
39 changes: 39 additions & 0 deletions test/apps/projection/data.ts
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;
}
18 changes: 18 additions & 0 deletions test/apps/projection/index.html
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>
16 changes: 16 additions & 0 deletions test/apps/projection/package.json
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"
}
}
Loading