@deck.gl-community/react 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/components/icon.d.ts +3 -0
- package/dist/components/icon.js +6 -0
- package/dist/components/long-press-button.d.ts +13 -0
- package/dist/components/long-press-button.js +31 -0
- package/dist/components/modal.d.ts +8 -0
- package/dist/components/modal.js +70 -0
- package/dist/components/positioned-view-control.d.ts +9 -0
- package/dist/components/positioned-view-control.js +5 -0
- package/dist/components/view-control.d.ts +38 -0
- package/dist/components/view-control.js +128 -0
- package/dist/index.cjs +521 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +13 -0
- package/dist/overlays/html-cluster-overlay.d.ts +13 -0
- package/dist/overlays/html-cluster-overlay.js +53 -0
- package/dist/overlays/html-overlay-item.d.ts +12 -0
- package/dist/overlays/html-overlay-item.js +13 -0
- package/dist/overlays/html-overlay.d.ts +19 -0
- package/dist/overlays/html-overlay.js +65 -0
- package/dist/overlays/html-tooltip-overlay.d.ts +16 -0
- package/dist/overlays/html-tooltip-overlay.js +53 -0
- package/package.json +50 -0
- package/src/components/icon.tsx +7 -0
- package/src/components/long-press-button.tsx +43 -0
- package/src/components/modal.tsx +92 -0
- package/src/components/positioned-view-control.tsx +16 -0
- package/src/components/view-control.tsx +163 -0
- package/src/index.ts +19 -0
- package/src/overlays/html-cluster-overlay.ts +80 -0
- package/src/overlays/html-overlay-item.tsx +30 -0
- package/src/overlays/html-overlay.tsx +91 -0
- package/src/overlays/html-tooltip-overlay.tsx +74 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
type Props = {
|
|
3
|
+
x?: number;
|
|
4
|
+
y?: number;
|
|
5
|
+
coordinates: number[];
|
|
6
|
+
children: any;
|
|
7
|
+
style?: Record<string, any>;
|
|
8
|
+
};
|
|
9
|
+
export declare class HtmlOverlayItem extends React.Component<Props> {
|
|
10
|
+
render(): React.JSX.Element;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export class HtmlOverlayItem extends React.Component {
|
|
3
|
+
render() {
|
|
4
|
+
const { x, y, children, style, coordinates, ...props } = this.props;
|
|
5
|
+
const { zIndex = 'auto', ...remainingStyle } = style || {};
|
|
6
|
+
return (
|
|
7
|
+
// Using transform translate to position overlay items will result in a smooth zooming
|
|
8
|
+
// effect, whereas using the top/left css properties will cause overlay items to
|
|
9
|
+
// jiggle when zooming
|
|
10
|
+
React.createElement("div", { style: { transform: `translate(${x}px, ${y}px)`, position: 'absolute', zIndex } },
|
|
11
|
+
React.createElement("div", { style: { userSelect: 'none', ...remainingStyle }, ...props }, children)));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
viewport?: Record<string, any>;
|
|
4
|
+
zIndex?: number;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
overflowMargin?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class HtmlOverlay extends React.Component<Props> {
|
|
9
|
+
static deckGLViewProps: boolean;
|
|
10
|
+
getItems(): Array<any>;
|
|
11
|
+
getCoords(coordinates: number[]): [number, number];
|
|
12
|
+
inView([x, y]: number[]): boolean;
|
|
13
|
+
scaleWithZoom(n: number): number;
|
|
14
|
+
breakpointWithZoom(threshold: number, a: any, b: any): any;
|
|
15
|
+
getViewport(): Record<string, any>;
|
|
16
|
+
getZoom(): any;
|
|
17
|
+
render(): React.JSX.Element;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
const styles = {
|
|
3
|
+
mainContainer: {
|
|
4
|
+
width: '100%',
|
|
5
|
+
height: '100%',
|
|
6
|
+
position: 'absolute',
|
|
7
|
+
pointerEvents: 'none',
|
|
8
|
+
overflow: 'hidden'
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export class HtmlOverlay extends React.Component {
|
|
12
|
+
// This is needed for Deck.gl 8.0+
|
|
13
|
+
static deckGLViewProps = true;
|
|
14
|
+
// Override this to provide your items
|
|
15
|
+
getItems() {
|
|
16
|
+
const { children } = this.props;
|
|
17
|
+
if (children) {
|
|
18
|
+
return Array.isArray(children) ? children : [children];
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
getCoords(coordinates) {
|
|
23
|
+
const pos = this.props.viewport.project(coordinates);
|
|
24
|
+
if (!pos)
|
|
25
|
+
return [-1, -1];
|
|
26
|
+
return pos;
|
|
27
|
+
}
|
|
28
|
+
inView([x, y]) {
|
|
29
|
+
const { viewport, overflowMargin = 0 } = this.props;
|
|
30
|
+
const { width, height } = viewport;
|
|
31
|
+
return !(x < -overflowMargin ||
|
|
32
|
+
y < -overflowMargin ||
|
|
33
|
+
x > width + overflowMargin ||
|
|
34
|
+
y > height + overflowMargin);
|
|
35
|
+
}
|
|
36
|
+
scaleWithZoom(n) {
|
|
37
|
+
const { zoom } = this.props.viewport;
|
|
38
|
+
return n / Math.pow(2, 20 - zoom);
|
|
39
|
+
}
|
|
40
|
+
breakpointWithZoom(threshold, a, b) {
|
|
41
|
+
const { zoom } = this.props.viewport;
|
|
42
|
+
return zoom > threshold ? a : b;
|
|
43
|
+
}
|
|
44
|
+
getViewport() {
|
|
45
|
+
return this.props.viewport;
|
|
46
|
+
}
|
|
47
|
+
getZoom() {
|
|
48
|
+
return this.props.viewport.zoom;
|
|
49
|
+
}
|
|
50
|
+
render() {
|
|
51
|
+
const { zIndex = 1 } = this.props;
|
|
52
|
+
const style = Object.assign({ zIndex }, styles.mainContainer);
|
|
53
|
+
const renderItems = [];
|
|
54
|
+
this.getItems()
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.forEach((item, index) => {
|
|
57
|
+
const [x, y] = this.getCoords(item.props.coordinates);
|
|
58
|
+
if (this.inView([x, y])) {
|
|
59
|
+
const key = item.key === null || item.key === undefined ? index : item.key;
|
|
60
|
+
renderItems.push(React.cloneElement(item, { x, y, key }));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return React.createElement("div", { style: style }, renderItems);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { HtmlOverlay } from './html-overlay';
|
|
3
|
+
type State = {
|
|
4
|
+
visible: boolean;
|
|
5
|
+
pickingInfo: Record<string, any> | null | undefined;
|
|
6
|
+
};
|
|
7
|
+
export declare class HtmlTooltipOverlay extends HtmlOverlay {
|
|
8
|
+
constructor(props: any);
|
|
9
|
+
componentWillMount(): void;
|
|
10
|
+
timeoutID: number | null | undefined;
|
|
11
|
+
state: State;
|
|
12
|
+
_getTooltip(pickingInfo: Record<string, any>): string;
|
|
13
|
+
_makeOverlay(): React.JSX.Element;
|
|
14
|
+
getItems(): Array<Record<string, any> | null | undefined>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { HtmlOverlay } from './html-overlay';
|
|
3
|
+
import { HtmlOverlayItem } from './html-overlay-item';
|
|
4
|
+
const styles = {
|
|
5
|
+
tooltip: {
|
|
6
|
+
transform: 'translate(-50%,-100%)',
|
|
7
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
8
|
+
padding: '4px 8px',
|
|
9
|
+
borderRadius: 8,
|
|
10
|
+
color: 'white'
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const SHOW_TOOLTIP_TIMEOUT = 250;
|
|
14
|
+
export class HtmlTooltipOverlay extends HtmlOverlay {
|
|
15
|
+
constructor(props) {
|
|
16
|
+
super(props);
|
|
17
|
+
this.state = { visible: false, pickingInfo: null };
|
|
18
|
+
}
|
|
19
|
+
componentWillMount() {
|
|
20
|
+
this.context.nebula.queryObjectEvents.on('pick', ({ event, pickingInfo }) => {
|
|
21
|
+
if (this.timeoutID !== null) {
|
|
22
|
+
window.clearTimeout(this.timeoutID);
|
|
23
|
+
}
|
|
24
|
+
this.timeoutID = null;
|
|
25
|
+
if (pickingInfo && this._getTooltip(pickingInfo)) {
|
|
26
|
+
this.timeoutID = window.setTimeout(() => {
|
|
27
|
+
this.setState({ visible: true, pickingInfo });
|
|
28
|
+
}, SHOW_TOOLTIP_TIMEOUT);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.setState({ visible: false });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
timeoutID = null;
|
|
36
|
+
state = undefined;
|
|
37
|
+
_getTooltip(pickingInfo) {
|
|
38
|
+
return pickingInfo.object.style.tooltip;
|
|
39
|
+
}
|
|
40
|
+
_makeOverlay() {
|
|
41
|
+
const { pickingInfo } = this.state;
|
|
42
|
+
if (pickingInfo) {
|
|
43
|
+
return (React.createElement(HtmlOverlayItem, { key: 0, coordinates: pickingInfo.lngLat, style: styles.tooltip }, this._getTooltip(pickingInfo)));
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
getItems() {
|
|
48
|
+
if (this.state.visible) {
|
|
49
|
+
return [this._makeOverlay()];
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deck.gl-community/react",
|
|
3
|
+
"description": "React components for deck.gl",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "9.0.1",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/uber/@deck.gl-community"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test-watch": "vitest"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"webgl",
|
|
16
|
+
"visualization",
|
|
17
|
+
"overlay",
|
|
18
|
+
"layer"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"require": "./dist/index.cjs",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@turf/helpers": "^6.5.0",
|
|
38
|
+
"@types/styled-react-modal": "^1.2.5",
|
|
39
|
+
"boxicons": "^2.1.4",
|
|
40
|
+
"prop-types": "^15.8.1",
|
|
41
|
+
"styled-components": "^4.4.1",
|
|
42
|
+
"styled-react-modal": "^3.1.1",
|
|
43
|
+
"supercluster": "^8.0.1"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^16.14 || ^17",
|
|
47
|
+
"react-dom": "^16.14 || ^17"
|
|
48
|
+
},
|
|
49
|
+
"gitHead": "646f8328d27d67d596f05c9154072051ec5cf4f0"
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React, {PureComponent} from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
|
|
4
|
+
export class LongPressButton extends PureComponent {
|
|
5
|
+
static propTypes = {
|
|
6
|
+
onClick: PropTypes.func.isRequired,
|
|
7
|
+
// eslint-disable-next-line react/forbid-prop-types
|
|
8
|
+
children: PropTypes.any.isRequired
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
buttonPressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
12
|
+
|
|
13
|
+
_repeat = () => {
|
|
14
|
+
if (this.buttonPressTimer) {
|
|
15
|
+
(this.props as any).onClick();
|
|
16
|
+
this.buttonPressTimer = setTimeout(this._repeat, 100);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
_handleButtonPress = () => {
|
|
21
|
+
this.buttonPressTimer = setTimeout(this._repeat, 100);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
_handleButtonRelease = () => {
|
|
25
|
+
if (this.buttonPressTimer) {
|
|
26
|
+
clearTimeout(this.buttonPressTimer);
|
|
27
|
+
}
|
|
28
|
+
this.buttonPressTimer = null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
render() {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
onMouseDown={(event) => {
|
|
35
|
+
this._handleButtonPress();
|
|
36
|
+
document.addEventListener('mouseup', this._handleButtonRelease, {once: true});
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{(this.props as any).children}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* eslint-env browser */
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import Modal, {ModalProvider} from 'styled-react-modal';
|
|
5
|
+
import styled from 'styled-components';
|
|
6
|
+
|
|
7
|
+
export const Button = styled.button`
|
|
8
|
+
display: inline-block;
|
|
9
|
+
color: #fff;
|
|
10
|
+
background-color: rgb(90, 98, 94);
|
|
11
|
+
font-size: 1em;
|
|
12
|
+
margin: 0.25em;
|
|
13
|
+
padding: 0.375em 0.75em;
|
|
14
|
+
border: 1px solid transparent;
|
|
15
|
+
border-radius: 0.25em;
|
|
16
|
+
display: block;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const StyledModal = Modal.styled`
|
|
20
|
+
position: relative;
|
|
21
|
+
display: block;
|
|
22
|
+
width: 50rem;
|
|
23
|
+
height: auto;
|
|
24
|
+
max-width: 500px;
|
|
25
|
+
margin: 1.75rem auto;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
28
|
+
font-size: 1rem;
|
|
29
|
+
font-weight: 400;
|
|
30
|
+
color: rgb(21, 25, 29);
|
|
31
|
+
line-height: 1.5;
|
|
32
|
+
text-align: left;
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const Content = styled.div`
|
|
36
|
+
position: relative;
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
width: 100%;
|
|
40
|
+
pointer-events: auto;
|
|
41
|
+
background-color: #fff;
|
|
42
|
+
background-clip: padding-box;
|
|
43
|
+
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
44
|
+
border-radius: 0.3rem;
|
|
45
|
+
outline: 0;
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const HeaderRow = styled.div`
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: flex-start;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
padding: 0.75rem 0.75rem;
|
|
53
|
+
border-bottom: 1px solid rgb(222, 226, 230);
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const Header = styled.h5`
|
|
57
|
+
font-size: 1.25rem;
|
|
58
|
+
font-weight: 500;
|
|
59
|
+
margin: 0;
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export type ModalProps = {
|
|
63
|
+
title: any;
|
|
64
|
+
content: any;
|
|
65
|
+
onClose: () => unknown;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function EditorModal(props: ModalProps) {
|
|
69
|
+
const [isOpen, setIsOpen] = React.useState(true);
|
|
70
|
+
|
|
71
|
+
function toggleModal() {
|
|
72
|
+
if (isOpen) {
|
|
73
|
+
props.onClose();
|
|
74
|
+
}
|
|
75
|
+
setIsOpen(!isOpen);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<ModalProvider>
|
|
81
|
+
<StyledModal isOpen={isOpen} onBackgroundClick={toggleModal} onEscapeKeydown={toggleModal}>
|
|
82
|
+
<Content>
|
|
83
|
+
<HeaderRow>
|
|
84
|
+
<Header>{props.title}</Header>
|
|
85
|
+
</HeaderRow>
|
|
86
|
+
{props.content}
|
|
87
|
+
</Content>
|
|
88
|
+
</StyledModal>
|
|
89
|
+
</ModalProvider>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {ViewControl} from './view-control';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
// A wrapper for positioning the ViewControl component
|
|
5
|
+
export const PositionedViewControl = ({fitBounds, panBy, zoomBy, zoomLevel, maxZoom, minZoom}) => (
|
|
6
|
+
<div style={{position: 'relative', top: '20px', left: '20px'}}>
|
|
7
|
+
<ViewControl
|
|
8
|
+
fitBounds={fitBounds}
|
|
9
|
+
panBy={panBy}
|
|
10
|
+
zoomBy={zoomBy}
|
|
11
|
+
zoomLevel={zoomLevel}
|
|
12
|
+
maxZoom={maxZoom}
|
|
13
|
+
minZoom={minZoom}
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// @ts-nocheck TODO
|
|
2
|
+
|
|
3
|
+
import React, {PureComponent} from 'react';
|
|
4
|
+
import PropTypes from 'prop-types';
|
|
5
|
+
import styled from 'styled-components';
|
|
6
|
+
import {LongPressButton} from './long-press-button';
|
|
7
|
+
|
|
8
|
+
export const ViewControlWrapper = styled.div`
|
|
9
|
+
align-items: center;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
position: absolute;
|
|
13
|
+
z-index: 99;
|
|
14
|
+
user-select: none;
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export const NavigationButtonContainer = styled.div`
|
|
18
|
+
background: #f7f7f7;
|
|
19
|
+
border-radius: 23px;
|
|
20
|
+
border: 0.5px solid #eaeaea;
|
|
21
|
+
box-shadow: inset 11px 11px 5px -7px rgba(230, 230, 230, 0.49);
|
|
22
|
+
height: 46px;
|
|
23
|
+
width: 46px;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export const NavigationButton = styled.div`
|
|
27
|
+
color: #848484;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
left: ${(props: any) => props.left};
|
|
30
|
+
position: absolute;
|
|
31
|
+
top: ${(props: any) => props.top};
|
|
32
|
+
transform: rotate(${(props: any) => props.rotate || 0}deg);
|
|
33
|
+
|
|
34
|
+
&:hover,
|
|
35
|
+
&:active {
|
|
36
|
+
color: #00ade6;
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export const ZoomControlWrapper = styled.div`
|
|
41
|
+
align-items: center;
|
|
42
|
+
background: #f7f7f7;
|
|
43
|
+
border: 0.5px solid #eaeaea;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
margin-top: 6px;
|
|
47
|
+
padding: 2px 0;
|
|
48
|
+
width: 18px;
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
export const VerticalSlider = styled.div`
|
|
52
|
+
display: inline-block;
|
|
53
|
+
height: 100px;
|
|
54
|
+
padding: 0;
|
|
55
|
+
width: 10px;
|
|
56
|
+
|
|
57
|
+
> input[type='range'][orient='vertical'] {
|
|
58
|
+
-webkit-appearance: slider-vertical;
|
|
59
|
+
height: 100px;
|
|
60
|
+
padding: 0;
|
|
61
|
+
margin: 0;
|
|
62
|
+
width: 10px;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
export const ZoomControlButton = styled.div`
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
font-size: 14px;
|
|
69
|
+
font-weight: 500;
|
|
70
|
+
margin: -4px;
|
|
71
|
+
|
|
72
|
+
&:hover,
|
|
73
|
+
&:active {
|
|
74
|
+
color: #00ade6;
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
export class ViewControl extends PureComponent {
|
|
79
|
+
static displayName = 'ViewControl';
|
|
80
|
+
|
|
81
|
+
static propTypes = {
|
|
82
|
+
// functions
|
|
83
|
+
fitBounds: PropTypes.func,
|
|
84
|
+
panBy: PropTypes.func,
|
|
85
|
+
zoomBy: PropTypes.func,
|
|
86
|
+
// current zoom level
|
|
87
|
+
zoomLevel: PropTypes.number,
|
|
88
|
+
// configuration
|
|
89
|
+
minZoom: PropTypes.number,
|
|
90
|
+
maxZoom: PropTypes.number,
|
|
91
|
+
deltaPan: PropTypes.number,
|
|
92
|
+
deltaZoom: PropTypes.number
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
static defaultProps = {
|
|
96
|
+
fitBounds: () => {},
|
|
97
|
+
panBy: () => {},
|
|
98
|
+
zoomBy: () => {},
|
|
99
|
+
deltaPan: 10,
|
|
100
|
+
deltaZoom: 0.1,
|
|
101
|
+
minZoom: 0.1,
|
|
102
|
+
maxZoom: 1
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// pan actions
|
|
106
|
+
panUp = () => (this.props as any).panBy(0, (this.props as any).deltaPan);
|
|
107
|
+
panDown = () => (this.props as any).panBy(0, -1 * (this.props as any).deltaPan);
|
|
108
|
+
panLeft = () => (this.props as any).panBy((this.props as any).deltaPan, 0);
|
|
109
|
+
panRight = () => (this.props as any).panBy(-1 * (this.props as any).deltaPan, 0);
|
|
110
|
+
|
|
111
|
+
// zoom actions
|
|
112
|
+
zoomIn = () => (this.props as any).zoomBy((this.props as any).deltaZoom);
|
|
113
|
+
zoomOut = () => (this.props as any).zoomBy(-1 * (this.props as any).deltaZoom);
|
|
114
|
+
onChangeZoomLevel = (evt) => {
|
|
115
|
+
const delta = evt.target.value - (this.props as any).zoomLevel;
|
|
116
|
+
(this.props as any).zoomBy(delta);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
render() {
|
|
120
|
+
const buttons = [
|
|
121
|
+
{top: -2, left: 14, rotate: 0, onClick: this.panUp, content: '▲', key: 'up'},
|
|
122
|
+
{top: 12, left: 0, rotate: -90, onClick: this.panLeft, content: '◀', key: 'left'},
|
|
123
|
+
{top: 12, left: 28, rotate: 90, onClick: this.panRight, content: '▶', key: 'right'},
|
|
124
|
+
{top: 25, left: 14, rotate: 180, onClick: this.panDown, content: '▼', key: 'down'}
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<ViewControlWrapper>
|
|
129
|
+
<NavigationButtonContainer>
|
|
130
|
+
{buttons.map((b: any) => (
|
|
131
|
+
<NavigationButton key={b.key} top={`${b.top}px`} left={`${b.left}px`} rotate={b.rotate}>
|
|
132
|
+
<LongPressButton onClick={b.onClick}>{b.content}</LongPressButton>
|
|
133
|
+
</NavigationButton>
|
|
134
|
+
))}
|
|
135
|
+
{/* @ts-expect-error TODO */}
|
|
136
|
+
<NavigationButton top={'12px'} left={'16px'} onClick={this.props.fitBounds}>
|
|
137
|
+
{'¤'}
|
|
138
|
+
</NavigationButton>
|
|
139
|
+
</NavigationButtonContainer>
|
|
140
|
+
<ZoomControlWrapper>
|
|
141
|
+
<ZoomControlButton>
|
|
142
|
+
<LongPressButton onClick={this.zoomIn}>{'+'}</LongPressButton>
|
|
143
|
+
</ZoomControlButton>
|
|
144
|
+
<VerticalSlider>
|
|
145
|
+
<input
|
|
146
|
+
type="range"
|
|
147
|
+
value={(this.props as any).zoomLevel}
|
|
148
|
+
min={(this.props as any).minZoom}
|
|
149
|
+
max={(this.props as any).maxZoom}
|
|
150
|
+
step={(this.props as any).deltaZoom}
|
|
151
|
+
onChange={this.onChangeZoomLevel}
|
|
152
|
+
/* @ts-expect-error */
|
|
153
|
+
orient="vertical"
|
|
154
|
+
/>
|
|
155
|
+
</VerticalSlider>
|
|
156
|
+
<ZoomControlButton>
|
|
157
|
+
<LongPressButton onClick={this.zoomOut}>{'-'}</LongPressButton>
|
|
158
|
+
</ZoomControlButton>
|
|
159
|
+
</ZoomControlWrapper>
|
|
160
|
+
</ViewControlWrapper>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Components (originally from @deck.gl-community/react-graph-layers)
|
|
2
|
+
|
|
3
|
+
export {LongPressButton} from './components/long-press-button';
|
|
4
|
+
export {ViewControl} from './components/view-control';
|
|
5
|
+
export {PositionedViewControl} from './components/positioned-view-control';
|
|
6
|
+
|
|
7
|
+
// Components (originally from @nebula.gl/editor)
|
|
8
|
+
|
|
9
|
+
export {EditorModal as Modal} from './components/modal';
|
|
10
|
+
export {Button} from './components/modal';
|
|
11
|
+
export {Icon} from './components/icon';
|
|
12
|
+
|
|
13
|
+
// Overlays (originally from @nebula.gl/overlays)
|
|
14
|
+
|
|
15
|
+
export {HtmlOverlay} from './overlays/html-overlay';
|
|
16
|
+
export {HtmlOverlayItem} from './overlays/html-overlay-item';
|
|
17
|
+
export {HtmlClusterOverlay} from './overlays/html-cluster-overlay';
|
|
18
|
+
|
|
19
|
+
export {HtmlTooltipOverlay} from './overlays/html-tooltip-overlay';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {point} from '@turf/helpers';
|
|
2
|
+
import Supercluster from 'supercluster';
|
|
3
|
+
import {HtmlOverlay} from './html-overlay';
|
|
4
|
+
|
|
5
|
+
export class HtmlClusterOverlay<ObjType> extends HtmlOverlay {
|
|
6
|
+
_superCluster: Supercluster;
|
|
7
|
+
_lastObjects: ObjType[] | null | undefined = null;
|
|
8
|
+
|
|
9
|
+
getItems(): Record<string, any>[] {
|
|
10
|
+
// supercluster().load() is expensive and we want to run it only
|
|
11
|
+
// when necessary and not for every frame.
|
|
12
|
+
|
|
13
|
+
// TODO: Warn if this is running many times / sec
|
|
14
|
+
|
|
15
|
+
const newObjects = this.getAllObjects();
|
|
16
|
+
if (newObjects !== this._lastObjects) {
|
|
17
|
+
this._superCluster = new Supercluster(this.getClusterOptions());
|
|
18
|
+
this._superCluster.load(
|
|
19
|
+
newObjects.map((object) => point(this.getObjectCoordinates(object), {object}))
|
|
20
|
+
);
|
|
21
|
+
this._lastObjects = newObjects;
|
|
22
|
+
// console.log('new Supercluster() run');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const clusters = this._superCluster.getClusters(
|
|
26
|
+
[-180, -90, 180, 90],
|
|
27
|
+
Math.round(this.getZoom())
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return clusters.map(
|
|
31
|
+
({
|
|
32
|
+
geometry: {coordinates},
|
|
33
|
+
properties: {cluster, point_count: pointCount, cluster_id: clusterId, object}
|
|
34
|
+
}) =>
|
|
35
|
+
cluster
|
|
36
|
+
? this.renderCluster(coordinates, clusterId, pointCount)
|
|
37
|
+
: this.renderObject(coordinates, object)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getClusterObjects(clusterId: number): ObjType[] {
|
|
42
|
+
return this._superCluster
|
|
43
|
+
.getLeaves(clusterId, Infinity)
|
|
44
|
+
.map((object) => object.properties.object);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Override to provide items that need clustering.
|
|
48
|
+
// If the items have not changed please provide the same array to avoid
|
|
49
|
+
// regeneration of the cluster which causes performance issues.
|
|
50
|
+
getAllObjects(): ObjType[] {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// override to provide coordinates for each object of getAllObjects()
|
|
55
|
+
getObjectCoordinates(obj: ObjType): [number, number] {
|
|
56
|
+
return [0, 0];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get options object used when instantiating supercluster
|
|
60
|
+
getClusterOptions(): Record<string, any> | null | undefined {
|
|
61
|
+
return {
|
|
62
|
+
maxZoom: 20
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// override to return an HtmlOverlayItem
|
|
67
|
+
renderObject(coordinates: number[], obj: ObjType): Record<string, any> | null | undefined {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// override to return an HtmlOverlayItem
|
|
72
|
+
// use getClusterObjects() to get cluster contents
|
|
73
|
+
renderCluster(
|
|
74
|
+
coordinates: number[],
|
|
75
|
+
clusterId: number,
|
|
76
|
+
pointCount: number
|
|
77
|
+
): Record<string, any> | null | undefined {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
// Injected by HtmlOverlay
|
|
5
|
+
x?: number;
|
|
6
|
+
y?: number;
|
|
7
|
+
|
|
8
|
+
// User provided
|
|
9
|
+
coordinates: number[];
|
|
10
|
+
children: any;
|
|
11
|
+
style?: Record<string, any>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class HtmlOverlayItem extends React.Component<Props> {
|
|
15
|
+
render() {
|
|
16
|
+
const {x, y, children, style, coordinates, ...props} = this.props;
|
|
17
|
+
const {zIndex = 'auto', ...remainingStyle} = style || {};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
// Using transform translate to position overlay items will result in a smooth zooming
|
|
21
|
+
// effect, whereas using the top/left css properties will cause overlay items to
|
|
22
|
+
// jiggle when zooming
|
|
23
|
+
<div style={{transform: `translate(${x}px, ${y}px)`, position: 'absolute', zIndex}}>
|
|
24
|
+
<div style={{userSelect: 'none', ...remainingStyle}} {...props}>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|