Skip to content

Commit f45c0a5

Browse files
authored
polygon layer (#79)
* wip: polygon layer * Updates for latest main * lint * remove own copy of exterior * special case for getPolygonOffset * updates * updates * add comment * remove unused imports
1 parent b128f31 commit f45c0a5

File tree

4 files changed

+318
-8
lines changed

4 files changed

+318
-8
lines changed

examples/polygon/app.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import React, { useState, useEffect } from "react";
22
import { createRoot } from "react-dom/client";
33
import { StaticMap, MapContext, NavigationControl } from "react-map-gl";
44
import DeckGL, { Layer, PickingInfo } from "deck.gl/typed";
5-
import { GeoArrowSolidPolygonLayer } from "@geoarrow/deck.gl-layers";
5+
import { GeoArrowPolygonLayer } from "@geoarrow/deck.gl-layers";
66
import * as arrow from "apache-arrow";
77

8-
const GEOARROW_POLYGON_DATA = "http://localhost:8080/utah.feather";
8+
const GEOARROW_POLYGON_DATA = "http://localhost:8080/small.feather";
99

1010
const INITIAL_VIEW_STATE = {
1111
latitude: 40.63403641639511,
@@ -38,7 +38,9 @@ function Root() {
3838
const data = await fetch(GEOARROW_POLYGON_DATA);
3939
const buffer = await data.arrayBuffer();
4040
const table = arrow.tableFromIPC(buffer);
41-
setTable(table);
41+
const table2 = new arrow.Table(table.batches.slice(0, 10));
42+
window.table = table2;
43+
setTable(table2);
4244
};
4345

4446
if (!table) {
@@ -50,11 +52,20 @@ function Root() {
5052

5153
table &&
5254
layers.push(
53-
new GeoArrowSolidPolygonLayer({
55+
new GeoArrowPolygonLayer({
5456
id: "geoarrow-polygons",
57+
stroked: true,
58+
filled: true,
5559
data: table,
56-
getFillColor: [0, 100, 60, 255],
57-
pickable: true,
60+
getFillColor: [0, 100, 60, 160],
61+
getLineColor: [255, 0, 0],
62+
lineWidthMinPixels: 0.1,
63+
extruded: false,
64+
wireframe: true,
65+
// getElevation: 0,
66+
pickable: false,
67+
positionFormat: "XY",
68+
_normalize: false,
5869
autoHighlight: true,
5970
earcutWorkerUrl: new URL(
6071
"https://cdn.jsdelivr.net/npm/@geoarrow/[email protected]/dist/earcut-worker.min.js",

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { GeoArrowColumnLayer } from "./column-layer.js";
33
export { GeoArrowH3HexagonLayer as _GeoArrowH3HexagonLayer } from "./h3-hexagon-layer.js";
44
export { GeoArrowHeatmapLayer } from "./heatmap-layer.js";
55
export { GeoArrowPathLayer } from "./path-layer.js";
6+
export { GeoArrowPolygonLayer } from "./polygon-layer.js";
67
export { GeoArrowScatterplotLayer } from "./scatterplot-layer.js";
78
export { GeoArrowSolidPolygonLayer } from "./solid-polygon-layer.js";
89
export { GeoArrowTextLayer as _GeoArrowTextLayer } from "./text-layer.js";
@@ -13,6 +14,7 @@ export type { GeoArrowColumnLayerProps } from "./column-layer.js";
1314
export type { GeoArrowH3HexagonLayerProps as _GeoArrowH3HexagonLayerProps } from "./h3-hexagon-layer.js";
1415
export type { GeoArrowHeatmapLayerProps } from "./heatmap-layer.js";
1516
export type { GeoArrowPathLayerProps } from "./path-layer.js";
17+
export type { GeoArrowPolygonLayerProps } from "./polygon-layer.js";
1618
export type { GeoArrowScatterplotLayerProps } from "./scatterplot-layer.js";
1719
export type { GeoArrowSolidPolygonLayerProps } from "./solid-polygon-layer.js";
1820
export type { GeoArrowTextLayerProps as _GeoArrowTextLayerProps } from "./text-layer.js";

src/polygon-layer.ts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import {
2+
CompositeLayer,
3+
CompositeLayerProps,
4+
DefaultProps,
5+
Layer,
6+
LayersList,
7+
GetPickingInfoParams,
8+
assert,
9+
} from "@deck.gl/core/typed";
10+
import { PolygonLayer } from "@deck.gl/layers/typed";
11+
import type { PolygonLayerProps } from "@deck.gl/layers/typed";
12+
import * as arrow from "apache-arrow";
13+
import * as ga from "@geoarrow/geoarrow-js";
14+
import { getGeometryVector } from "./utils.js";
15+
import { GeoArrowExtraPickingProps, getPickingInfo } from "./picking.js";
16+
import { ColorAccessor, FloatAccessor, GeoArrowPickingInfo } from "./types.js";
17+
import { EXTENSION_NAME } from "./constants.js";
18+
import { validateAccessors } from "./validate.js";
19+
import { GeoArrowSolidPolygonLayer } from "./solid-polygon-layer.js";
20+
import { GeoArrowPathLayer } from "./path-layer.js";
21+
22+
/** All properties supported by GeoArrowPolygonLayer */
23+
export type GeoArrowPolygonLayerProps = Omit<
24+
PolygonLayerProps,
25+
| "data"
26+
| "getPolygon"
27+
| "getFillColor"
28+
| "getLineColor"
29+
| "getLineWidth"
30+
| "getElevation"
31+
> &
32+
_GeoArrowPolygonLayerProps &
33+
CompositeLayerProps;
34+
35+
/** Properties added by GeoArrowPolygonLayer */
36+
type _GeoArrowPolygonLayerProps = {
37+
data: arrow.Table;
38+
39+
/** Polygon geometry accessor. */
40+
getPolygon?: ga.vector.PolygonVector | ga.vector.MultiPolygonVector;
41+
/** Fill color accessor.
42+
* @default [0, 0, 0, 255]
43+
*/
44+
getFillColor?: ColorAccessor;
45+
/** Stroke color accessor.
46+
* @default [0, 0, 0, 255]
47+
*/
48+
getLineColor?: ColorAccessor;
49+
/**
50+
* Line width value of accessor.
51+
* @default 1
52+
*/
53+
getLineWidth?: FloatAccessor;
54+
/**
55+
* Elevation value of accessor.
56+
*
57+
* Only used if `extruded: true`.
58+
*
59+
* @default 1000
60+
*/
61+
getElevation?: FloatAccessor;
62+
63+
/**
64+
* If `true`, validate the arrays provided (e.g. chunk lengths)
65+
* @default true
66+
*/
67+
_validate?: boolean;
68+
};
69+
70+
// Remove data and getPolygon from the upstream default props
71+
const {
72+
data: _data,
73+
getPolygon: _getPolygon,
74+
..._defaultProps
75+
} = PolygonLayer.defaultProps;
76+
77+
// Default props added by us
78+
const ourDefaultProps: Pick<
79+
GeoArrowPolygonLayerProps,
80+
"_normalize" | "_windingOrder" | "_validate"
81+
> = {
82+
// Note: this diverges from upstream, where here we default to no
83+
// normalization
84+
_normalize: false,
85+
// Note: this diverges from upstream, where here we default to CCW
86+
_windingOrder: "CCW",
87+
88+
_validate: true,
89+
};
90+
91+
// @ts-expect-error Type error in merging default props with ours
92+
const defaultProps: DefaultProps<GeoArrowPolygonLayerProps> = {
93+
..._defaultProps,
94+
...ourDefaultProps,
95+
};
96+
97+
const defaultLineColor: [number, number, number, number] = [0, 0, 0, 255];
98+
const defaultFillColor: [number, number, number, number] = [0, 0, 0, 255];
99+
100+
export class GeoArrowPolygonLayer<
101+
ExtraProps extends {} = {},
102+
> extends CompositeLayer<Required<GeoArrowPolygonLayerProps> & ExtraProps> {
103+
static defaultProps = defaultProps;
104+
static layerName = "GeoArrowPolygonLayer";
105+
106+
getPickingInfo(
107+
params: GetPickingInfoParams & {
108+
sourceLayer: { props: GeoArrowExtraPickingProps };
109+
},
110+
): GeoArrowPickingInfo {
111+
return getPickingInfo(params, this.props.data);
112+
}
113+
114+
renderLayers(): Layer<{}> | LayersList | null {
115+
const { data: table } = this.props;
116+
117+
const polygonVector = getGeometryVector(table, EXTENSION_NAME.POLYGON);
118+
if (polygonVector !== null) {
119+
return this._renderLayers(polygonVector);
120+
}
121+
122+
const MultiPolygonVector = getGeometryVector(
123+
table,
124+
EXTENSION_NAME.MULTIPOLYGON,
125+
);
126+
if (MultiPolygonVector !== null) {
127+
return this._renderLayers(MultiPolygonVector);
128+
}
129+
130+
const geometryColumn = this.props.getPolygon;
131+
if (ga.vector.isPolygonVector(geometryColumn)) {
132+
return this._renderLayers(geometryColumn);
133+
}
134+
135+
if (ga.vector.isMultiPolygonVector(geometryColumn)) {
136+
return this._renderLayers(geometryColumn);
137+
}
138+
139+
throw new Error("geometryColumn not Polygon or MultiPolygon");
140+
}
141+
142+
// NOTE: Here we shouldn't need a split for handling both multi- and single-
143+
// geometries, because the underlying SolidPolygonLayer and PathLayer both
144+
// support multi-* and single- geometries.
145+
_renderLayers(
146+
geometryColumn: ga.vector.PolygonVector | ga.vector.MultiPolygonVector,
147+
): Layer<{}> | LayersList | null {
148+
const { data: table } = this.props;
149+
150+
if (this.props._validate) {
151+
assert(ga.vector.isPolygonVector(geometryColumn));
152+
validateAccessors(this.props, table);
153+
}
154+
155+
let getPath: ga.vector.LineStringVector | ga.vector.MultiLineStringVector;
156+
if (ga.vector.isPolygonVector(geometryColumn)) {
157+
getPath = ga.algorithm.getPolygonExterior(geometryColumn);
158+
} else if (ga.vector.isMultiPolygonVector(geometryColumn)) {
159+
getPath = ga.algorithm.getMultiPolygonExterior(geometryColumn);
160+
} else {
161+
assert(false);
162+
}
163+
164+
// Layer composition props
165+
const {
166+
data,
167+
_dataDiff,
168+
stroked,
169+
filled,
170+
extruded,
171+
wireframe,
172+
_normalize,
173+
_windingOrder,
174+
elevationScale,
175+
transitions,
176+
positionFormat,
177+
} = this.props;
178+
179+
// Rendering props underlying layer
180+
const {
181+
lineWidthUnits,
182+
lineWidthScale,
183+
lineWidthMinPixels,
184+
lineWidthMaxPixels,
185+
lineJointRounded,
186+
lineMiterLimit,
187+
lineDashJustified,
188+
} = this.props;
189+
190+
// Accessor props for underlying layers
191+
const {
192+
getFillColor,
193+
getLineColor,
194+
getLineWidth,
195+
getElevation,
196+
getPolygon,
197+
updateTriggers,
198+
material,
199+
} = this.props;
200+
201+
const FillLayer = this.getSubLayerClass("fill", GeoArrowSolidPolygonLayer);
202+
const StrokeLayer = this.getSubLayerClass("stroke", GeoArrowPathLayer);
203+
204+
// Filled Polygon Layer
205+
const polygonLayer = new FillLayer(
206+
{
207+
// _dataDiff,
208+
extruded,
209+
elevationScale,
210+
211+
filled,
212+
wireframe,
213+
_normalize,
214+
_windingOrder,
215+
216+
getElevation,
217+
getFillColor,
218+
getLineColor: extruded && wireframe ? getLineColor : defaultLineColor,
219+
220+
material,
221+
transitions,
222+
},
223+
this.getSubLayerProps({
224+
id: "fill",
225+
updateTriggers: updateTriggers && {
226+
getPolygon: updateTriggers.getPolygon,
227+
getElevation: updateTriggers.getElevation,
228+
getFillColor: updateTriggers.getFillColor,
229+
getLineColor: updateTriggers.getLineColor,
230+
},
231+
}),
232+
{
233+
data,
234+
positionFormat,
235+
getPolygon,
236+
},
237+
);
238+
239+
// Polygon line layer
240+
const polygonLineLayer =
241+
!extruded &&
242+
stroked &&
243+
new StrokeLayer(
244+
{
245+
// _dataDiff,
246+
widthUnits: lineWidthUnits,
247+
widthScale: lineWidthScale,
248+
widthMinPixels: lineWidthMinPixels,
249+
widthMaxPixels: lineWidthMaxPixels,
250+
jointRounded: lineJointRounded,
251+
miterLimit: lineMiterLimit,
252+
dashJustified: lineDashJustified,
253+
254+
// Already normalized, and since they had been polygons, we know that
255+
// the lines are a loop.
256+
_pathType: "loop",
257+
258+
transitions: transitions && {
259+
getWidth: transitions.getLineWidth,
260+
getColor: transitions.getLineColor,
261+
getPath: transitions.getPolygon,
262+
},
263+
264+
getColor: this.getSubLayerAccessor(getLineColor),
265+
getWidth: this.getSubLayerAccessor(getLineWidth),
266+
},
267+
this.getSubLayerProps({
268+
id: "stroke",
269+
updateTriggers: updateTriggers && {
270+
getWidth: updateTriggers.getLineWidth,
271+
getColor: updateTriggers.getLineColor,
272+
getDashArray: updateTriggers.getLineDashArray,
273+
},
274+
}),
275+
{
276+
data: table,
277+
positionFormat,
278+
getPath,
279+
},
280+
);
281+
282+
const layers = [
283+
// If not extruded: flat fill layer is drawn below outlines
284+
!extruded && polygonLayer,
285+
polygonLineLayer,
286+
// If extruded: draw fill layer last for correct blending behavior
287+
extruded && polygonLayer,
288+
];
289+
return layers;
290+
}
291+
}

src/utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,14 @@ export function assignAccessor(args: AssignAccessorProps) {
195195
};
196196
}
197197
} else if (typeof propInput === "function") {
198-
props[propName] = <In>(_object: null, objectInfo: AccessorContext<In>) =>
199-
wrapAccessorFunction(objectInfo, propInput);
198+
props[propName] = <In>(object: any, objectInfo: AccessorContext<In>) => {
199+
// Special case that doesn't have the same parameters
200+
if (propName === "getPolygonOffset") {
201+
return propInput(object, objectInfo);
202+
}
203+
204+
return wrapAccessorFunction(objectInfo, propInput);
205+
};
200206
} else {
201207
props[propName] = propInput;
202208
}

0 commit comments

Comments
 (0)