Skip to content

Commit

Permalink
Flesh out the Toolbox component (#484)
Browse files Browse the repository at this point in the history
This makes the `editor` example much closer to `advanced` example, but more usable. The `Toolbox` component is still hard-coded for now. But in the future, we plan to make it customizable.
  • Loading branch information
supersonicclay committed Oct 7, 2020
1 parent 764dcb8 commit e804189
Show file tree
Hide file tree
Showing 6 changed files with 391 additions and 34 deletions.
53 changes: 50 additions & 3 deletions examples/editor/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,49 @@ const initialViewState = {
export function Example() {
const [geoJson, setGeoJson] = React.useState({
type: 'FeatureCollection',
features: [],
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[-122.46212548792364, 37.79026033616934],
[-122.48435831844807, 37.77160302698496],
[-122.45884849905971, 37.74414218845571],
[-122.42863676726826, 37.76266965836386],
[-122.46212548792364, 37.79026033616934],
],
],
},
},
{
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[-122.4136573004723, 37.78826678755718],
[-122.44875601708893, 37.782670574261324],
[-122.43793598592286, 37.74322062447909],
[-122.40836932539945, 37.75125290412125],
[-122.4136573004723, 37.78826678755718],
],
],
},
},
],
});
const [selectedFeatureIndexes] = React.useState([]);
const [selectedFeatureIndexes, setSelectedFeatureIndexes] = React.useState([0]);
const [mode, setMode] = React.useState(() => ViewMode);
const [modeConfig, setModeConfig] = React.useState({});

const layer = new EditableGeoJsonLayer({
data: geoJson,
mode,
modeConfig,
selectedFeatureIndexes,
onEdit: ({ updatedData }) => {
setGeoJson(updatedData);
Expand All @@ -42,19 +77,31 @@ export function Example() {
}}
layers={[layer]}
getCursor={layer.getCursor.bind(layer)}
onClick={(info) => {
if (mode === ViewMode)
if (info) {
setSelectedFeatureIndexes([info.index]);
} else {
setSelectedFeatureIndexes([]);
}
}}
>
<StaticMap mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN} />
</DeckGL>

<Toolbox
mode={mode}
geoJson={geoJson}
mode={mode}
modeConfig={modeConfig}
onSetMode={setMode}
onSetModeConfig={setModeConfig}
onImport={(imported) =>
setGeoJson({
...geoJson,
features: [...geoJson.features, ...imported.features],
})
}
onSetGeoJson={setGeoJson}
/>
</>
);
Expand Down
1 change: 1 addition & 0 deletions modules/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@types/downloadjs": "1.4.1",
"@types/styled-react-modal": "1.2.0",
"@types/wellknown": "0.5.1",
"boxicons": "^2.0.5",
"clipboard-copy": "^3.1.0",
"downloadjs": "^1.4.7",
"react": "^16.8.0",
Expand Down
7 changes: 7 additions & 0 deletions modules/editor/src/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';
import 'boxicons';

export function Icon(props) {
// @ts-ignore
return <box-icon color="currentColor" {...props} />;
}
212 changes: 190 additions & 22 deletions modules/editor/src/toolbox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import * as React from 'react';
import { ViewMode, DrawPointMode, DrawPolygonMode } from '@nebula.gl/edit-modes';
import {
ViewMode,
DrawPointMode,
DrawLineStringMode,
DrawPolygonMode,
DrawCircleFromCenterMode,
DrawRectangleMode,
MeasureDistanceMode,
MeasureAngleMode,
MeasureAreaMode,
} from '@nebula.gl/edit-modes';
import styled from 'styled-components';
import { Icon } from './icon';

import { ImportModal } from './import-modal';
import { ExportModal } from './export-modal';

Expand All @@ -12,9 +24,10 @@ const Tools = styled.div`
right: 10px;
`;

const Button = styled.button<{ active?: boolean }>`
const Button = styled.button<{ active?: boolean; kind?: string }>`
color: #fff;
background: ${({ active }) => (active ? 'rgb(0, 105, 217)' : 'rgb(90, 98, 94)')};
background: ${({ kind, active }) =>
kind === 'danger' ? 'rgb(180, 40, 40)' : active ? 'rgb(0, 105, 217)' : 'rgb(90, 98, 94)'};
font-size: 1em;
font-weight: 400;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
Expand All @@ -29,42 +42,196 @@ const Button = styled.button<{ active?: boolean }>`
}
`;

const SubToolsContainer = styled.div`
position: relative;
`;

const SubTools = styled.div`
display: flex;
flex-direction: row-reverse;
position: absolute;
top: 0;
right: 0;
`;

export type Props = {
mode: any;
modeConfig: any;
geoJson: any;
onSetMode: (mode: any) => unknown;
onSetModeConfig: (modeConfig: any) => unknown;
onSetGeoJson: (geojson: any) => unknown;
onImport: (imported: any) => unknown;
};

const MODE_BUTTONS = [
// TODO: change these to icons
{ mode: ViewMode, content: 'View' },
{ mode: DrawPointMode, content: 'Draw Point' },
{ mode: DrawPolygonMode, content: 'Draw Polygon' },
const MODE_GROUPS = [
{
modes: [{ mode: ViewMode, content: <Icon name="pointer" /> }],
},
{
modes: [{ mode: DrawPointMode, content: <Icon name="map-pin" /> }],
},
{
modes: [
{
mode: DrawLineStringMode,
content: <Icon name="stats" />,
},
],
},
{
modes: [
{ mode: DrawPolygonMode, content: <Icon name="shape-polygon" /> },
{ mode: DrawRectangleMode, content: <Icon name="rectangle" /> },
{ mode: DrawCircleFromCenterMode, content: <Icon name="circle" /> },
],
},
{
modes: [
{ mode: MeasureDistanceMode, content: <Icon name="ruler" /> },
{ mode: MeasureAngleMode, content: <Icon name="shape-triangle" /> },
{ mode: MeasureAreaMode, content: <Icon name="shape-square" /> },
],
},
];

export function Toolbox({ mode, geoJson, onSetMode, onImport }: Props) {
// Initialize to zero index on load as nothing is active.
function ModeButton({ buttonConfig, mode, onClick }: any) {
return (
<Button active={buttonConfig.mode === mode} onClick={onClick}>
{buttonConfig.content}
</Button>
);
}
function ModeGroupButtons({ modeGroup, mode, onSetMode }: any) {
const [expanded, setExpanded] = React.useState(false);

const { modes } = modeGroup;

let subTools = null;

if (expanded) {
subTools = (
<SubTools>
{modes.map((buttonConfig, i) => (
<ModeButton
key={i}
buttonConfig={buttonConfig}
mode={mode}
onClick={() => {
onSetMode(() => buttonConfig.mode);
setExpanded(false);
}}
/>
))}
</SubTools>
);
}

// Get the button config if it is active otherwise, choose the first
const buttonConfig = modes.find((m) => m.mode === mode) || modes[0];

return (
<SubToolsContainer>
{subTools}
<ModeButton
buttonConfig={buttonConfig}
mode={mode}
onClick={() => {
onSetMode(() => buttonConfig.mode);
setExpanded(true);
}}
/>
</SubToolsContainer>
);
}

export function Toolbox({
mode,
modeConfig,
geoJson,
onSetMode,
onSetModeConfig,
onSetGeoJson,
onImport,
}: Props) {
const [showConfig, setShowConfig] = React.useState(false);
const [showImport, setShowImport] = React.useState(false);
const [showExport, setShowExport] = React.useState(false);
const [showClearConfirmation, setShowClearConfirmation] = React.useState(false);

return (
<>
<Tools>
{MODE_BUTTONS.map((modeButton, i) => (
<Button
key={i}
active={mode === modeButton.mode}
onClick={() => {
onSetMode(() => modeButton.mode);
}}
>
{modeButton.content}
</Button>
{MODE_GROUPS.map((modeGroup, i) => (
<ModeGroupButtons key={i} modeGroup={modeGroup} mode={mode} onSetMode={onSetMode} />
))}
<Button onClick={() => setShowImport(true)}>Import Geometry</Button>
<Button onClick={() => setShowExport(true)}>Export Geometry</Button>

{/* <box-icon name='current-location' ></box-icon> */}
<Button onClick={() => setShowExport(true)} title="Export">
<Icon name="export" />
</Button>
<Button onClick={() => setShowImport(true)} title="Import">
<Icon name="import" />
</Button>

<SubToolsContainer>
{showConfig && (
<SubTools>
<Button onClick={() => setShowConfig(false)}>
<Icon name="chevron-right" />
</Button>
<Button
onClick={() => onSetModeConfig({ booleanOperation: 'difference' })}
active={modeConfig && modeConfig.booleanOperation === 'difference'}
>
<Icon name="minus-front" />
</Button>
<Button
onClick={() => onSetModeConfig({ booleanOperation: 'union' })}
active={modeConfig && modeConfig.booleanOperation === 'union'}
>
<Icon name="unite" />
</Button>
<Button
onClick={() => onSetModeConfig({ booleanOperation: 'intersection' })}
active={modeConfig && modeConfig.booleanOperation === 'intersection'}
>
<Icon name="intersect" />
</Button>
{/* <Button onClick={() => setShowConfig(false)}>
<Icon name="x" />
</Button> */}
</SubTools>
)}
<Button onClick={() => setShowConfig(true)}>
<Icon name="cog" />
</Button>
</SubToolsContainer>

<SubToolsContainer>
{showClearConfirmation && (
<SubTools>
<Button
onClick={() => {
onSetGeoJson({ type: 'FeatureCollection', features: [] });
setShowClearConfirmation(false);
}}
kind="danger"
title="Clear all features"
>
Clear all features <Icon name="trash" />
</Button>
<Button onClick={() => setShowClearConfirmation(false)}>Cancel</Button>
</SubTools>
)}
<Button onClick={() => setShowClearConfirmation(true)} title="Clear">
<Icon name="trash" />
</Button>
</SubToolsContainer>

{/* zoom in and out */}
</Tools>

{showImport && (
<ImportModal
onImport={(imported) => {
Expand All @@ -74,6 +241,7 @@ export function Toolbox({ mode, geoJson, onSetMode, onImport }: Props) {
onClose={() => setShowImport(false)}
/>
)}

{showExport && <ExportModal geoJson={geoJson} onClose={() => setShowExport(false)} />}
</>
);
Expand Down
12 changes: 7 additions & 5 deletions modules/layers/src/layers/editable-geojson-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ import {

import EditableLayer from './editable-layer';

const DEFAULT_LINE_COLOR = [0x0, 0x0, 0x0, 0xff];
const DEFAULT_LINE_COLOR = [0x0, 0x0, 0x0, 0x99];
const DEFAULT_FILL_COLOR = [0x0, 0x0, 0x0, 0x90];
const DEFAULT_SELECTED_LINE_COLOR = [0x90, 0x90, 0x90, 0xff];
const DEFAULT_SELECTED_FILL_COLOR = [0x90, 0x90, 0x90, 0x90];
const DEFAULT_SELECTED_LINE_COLOR = [0x0, 0x0, 0x0, 0xff];
const DEFAULT_SELECTED_FILL_COLOR = [0x0, 0x0, 0x90, 0x90];
const DEFAULT_TENTATIVE_LINE_COLOR = [0x90, 0x90, 0x90, 0xff];
const DEFAULT_TENTATIVE_FILL_COLOR = [0x90, 0x90, 0x90, 0x90];
const DEFAULT_EDITING_EXISTING_POINT_COLOR = [0xc0, 0x0, 0x0, 0xff];
const DEFAULT_EDITING_INTERMEDIATE_POINT_COLOR = [0x0, 0x0, 0x0, 0x80];
const DEFAULT_EDITING_SNAP_POINT_COLOR = [0x7c, 0x00, 0xc0, 0xff];
Expand Down Expand Up @@ -128,8 +130,8 @@ const defaultProps = {
getLineWidth: (f) => (f && f.properties && f.properties.lineWidth) || 3,

// Tentative feature rendering
getTentativeLineColor: (f) => DEFAULT_SELECTED_LINE_COLOR,
getTentativeFillColor: (f) => DEFAULT_SELECTED_FILL_COLOR,
getTentativeLineColor: (f) => DEFAULT_TENTATIVE_LINE_COLOR,
getTentativeFillColor: (f) => DEFAULT_TENTATIVE_FILL_COLOR,
getTentativeLineWidth: (f) => (f && f.properties && f.properties.lineWidth) || 3,

editHandleType: 'point',
Expand Down
Loading

0 comments on commit e804189

Please sign in to comment.