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

[Feat] enhance mouse selection toolset #2164

Merged
merged 4 commits into from
Mar 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/components/src/common/icons/cursor-point.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import Base, {BaseProps} from './base';

export default class CursorPoint extends Component<Partial<BaseProps>> {
static propTypes = {
/** Set the height of the icon, ex. '16px' */
height: PropTypes.string
};

static defaultProps = {
height: '16px',
predefinedClassName: 'data-ex-icons-cursorpoint'
};

render() {
return (
<Base {...this.props}>
<g transform="scale(4, 4)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.11516 0.69784C0.869317 0.556367 0.562398 0.57724 0.337968 0.750693C0.113538 0.924147 0.0159658 1.21589 0.0908917 1.48946L3.32832 13.31C3.40524 13.5909 3.648 13.7948 3.93792 13.8221C4.22784 13.8493 4.50435 13.6943 4.63228 13.4327L6.3441 9.93235L9.41359 13.9039C9.65 14.2098 10.0896 14.2661 10.3955 14.0297C10.7014 13.7933 10.7577 13.3537 10.5213 13.0478L7.35388 8.94949L11.5277 8.10342C11.8131 8.04556 12.0329 7.81707 12.0796 7.52964C12.1263 7.24222 11.9902 6.95589 11.7378 6.81065L1.11516 0.69784ZM4.18896 11.1525L1.89017 2.75907L9.43295 7.09958L6.15023 7.76501C5.93703 7.80823 5.75604 7.94811 5.66047 8.14353L4.18896 11.1525Z"
fill="#54638C"
/>
</g>
</Base>
);
}
}
1 change: 1 addition & 0 deletions src/components/src/common/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,5 @@ export {default as OrderByDataset} from './order-by-dataset';
export {default as Messages} from './messages';
export {default as Crosshairs} from './crosshairs';
export {default as CursorClick} from './cursor-click';
export {default as CursorPoint} from './cursor-point';
export {default as Pin} from './pin';
4 changes: 4 additions & 0 deletions src/components/src/map-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ export default function MapContainerFactory(
coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
frozen={true}
isBase={compareMode}
onSetFeatures={this.props.visStateActions.setFeatures}
setSelectedFeature={this.props.visStateActions.setSelectedFeature}
/>
)}
{layerHoverProp && (!layerPinnedProp || compareMode) && (
Expand All @@ -597,6 +599,8 @@ export default function MapContainerFactory(
layerHoverProp={layerHoverProp}
frozen={false}
coordinate={interactionConfig.coordinate.enabled && coordinate}
onSetFeatures={this.props.visStateActions.setFeatures}
setSelectedFeature={this.props.visStateActions.setSelectedFeature}
/>
)}
</ErrorBoundary>
Expand Down
15 changes: 13 additions & 2 deletions src/components/src/map/layer-hover-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,20 @@ const StyledTable = styled.table`
}
`;

const StyledDivider = styled.div`
// offset divider to reach popover edge
margin-left: -14px;
margin-right: -14px;
border-bottom: 1px solid ${props => props.theme.panelBorderColor};
`;

interface RowProps {
name: string;
value: string;
deltaValue?: string | null;
url?: string;
}

/** @type {import('./layer-hover-info').RowComponent} */
const Row: React.FC<RowProps> = ({name, value, deltaValue, url}) => {
// Set 'url' to 'value' if it looks like a url
if (!url && value && typeof value === 'string' && value.match(/^http/)) {
Expand Down Expand Up @@ -192,17 +198,21 @@ const LayerHoverInfoFactory = () => {
const LayerHoverInfo = props => {
const {data, layer} = props;
const intl = useIntl();

if (!data || !layer) {
return null;
}

const hasFieldsToShow =
(data.fieldValues && Object.keys(data.fieldValues).length > 0) ||
(props.fieldsToShow && props.fieldsToShow.length > 0);

return (
<div className="map-popover__layer-info">
<StyledLayerName className="map-popover__layer-name">
<Layers height="12px" />
{props.layer.config.label}
</StyledLayerName>
{hasFieldsToShow && <StyledDivider />}
<StyledTable>
{data.fieldValues ? (
<tbody>
Expand All @@ -216,6 +226,7 @@ const LayerHoverInfoFactory = () => {
<EntryInfo {...props} />
)}
</StyledTable>
{hasFieldsToShow && <StyledDivider />}
</div>
);
};
Expand Down
81 changes: 72 additions & 9 deletions src/components/src/map/map-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React, {useState} from 'react';
import React, {useState, useCallback} from 'react';
import styled from 'styled-components';
import LayerHoverInfoFactory from './layer-hover-info';
import CoordinateInfoFactory from './coordinate-info';
import MapPopoverContentFactory from './map-popover-content';
import {Pin, ArrowLeft, ArrowRight} from '../common/icons';
import {Pin, ArrowLeft, ArrowRight, CursorPoint} from '../common/icons';
import {injectIntl, IntlShape} from 'react-intl';
import {FormattedMessage} from '@kepler.gl/localization';
import Tippy from '@tippyjs/react/headless';
import {RootContext} from '../';
import {parseGeoJsonRawFeature} from '@kepler.gl/layers';
import {idToPolygonGeo, generateHashId} from '@kepler.gl/utils';
import {LAYER_TYPES} from '@kepler.gl/constants';
import {LayerHoverProp} from '@kepler.gl/reducers';
import {Feature} from '@kepler.gl/types';

const SELECTABLE_LAYERS: string[] = [LAYER_TYPES.hexagonId, LAYER_TYPES.geojson];
const MAX_WIDTH = 500;
const MAX_HEIGHT = 600;

Expand Down Expand Up @@ -127,7 +130,21 @@ const StyledIcon = styled.div`
}
`;

MapPopoverFactory.deps = [LayerHoverInfoFactory, CoordinateInfoFactory, MapPopoverContentFactory];
const StyledSelectGeometry = styled.div`
display: flex;
align-items: center;
color: ${props => props.theme.textColorHl};
svg {
margin-right: 6px;
}

:hover {
cursor: pointer;
color: ${props => props.theme.linkBtnColor};
}
`;

MapPopoverFactory.deps = [MapPopoverContentFactory];

function createVirtualReference(container, x, y, size = 0) {
const bounds =
Expand Down Expand Up @@ -176,6 +193,30 @@ function getPopperOptions(container) {
};
}

export function getSelectedFeature(layerHoverProp: LayerHoverProp | null): Feature | null {
const layer = layerHoverProp?.layer;
let fieldIdx;
let selectedFeature;
switch (layer?.type) {
case LAYER_TYPES.hexagonId:
fieldIdx = layer.config?.columns?.hex_id?.fieldIdx;
selectedFeature = idToPolygonGeo({id: layerHoverProp?.data?.[fieldIdx]}, {isClosed: true});
break;
case LAYER_TYPES.geojson:
fieldIdx = layer.config?.columns?.geojson?.fieldIdx;
selectedFeature = parseGeoJsonRawFeature(layerHoverProp?.data?.[fieldIdx]);
break;
default:
break;
}

return {
...selectedFeature,
// unique id should be assigned to features in the editor
id: generateHashId(8)
};
}

export type MapPopoverProps = {
x: number;
y: number;
Expand All @@ -186,18 +227,17 @@ export type MapPopoverProps = {
zoom: number;
container?: HTMLElement | null;
onClose: () => void;
onSetFeatures: (features: Feature[]) => any;
setSelectedFeature: (feature: Feature, clickContext: object) => any;
};

type IntlProps = {
intl: IntlShape;
};

export default function MapPopoverFactory(
LayerHoverInfo: ReturnType<typeof LayerHoverInfoFactory>,
CoordinateInfo: ReturnType<typeof CoordinateInfoFactory>,
MapPopoverContent: ReturnType<typeof MapPopoverContentFactory>
) {
/** @type {typeof import('./map-popover').MapPopover} */
const MapPopover: React.FC<MapPopoverProps & IntlProps> = ({
x,
y,
Expand All @@ -207,11 +247,28 @@ export default function MapPopoverFactory(
isBase,
zoom,
container,
onClose
onClose,
onSetFeatures,
setSelectedFeature
}) => {
const [horizontalPlacement, setHorizontalPlacement] = useState('start');
const moveLeft = () => setHorizontalPlacement('end');
const moveRight = () => setHorizontalPlacement('start');

const onSetSelectedFeature = useCallback(() => {
const clickContext = {
mapIndex: 0,
rightClick: true,
position: {x, y}
};
const selectedFeature = getSelectedFeature(layerHoverProp);
if (selectedFeature) {
setSelectedFeature(selectedFeature, clickContext);
onSetFeatures([selectedFeature]);
}
onClose();
}, [onClose, onSetFeatures, x, y, setSelectedFeature, layerHoverProp]);

return (
<RootContext.Consumer>
{context => (
Expand Down Expand Up @@ -258,6 +315,12 @@ export default function MapPopoverFactory(
layerHoverProp={layerHoverProp}
/>
</PopoverContent>
{SELECTABLE_LAYERS.includes(layerHoverProp?.layer?.type as string) && frozen ? (
<StyledSelectGeometry className="select-geometry" onClick={onSetSelectedFeature}>
<CursorPoint />
Select Geometry
</StyledSelectGeometry>
) : null}
</StyledMapPopover>
)}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/layers/src/editor-layer/editor-layer-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function onClick(
if (editor.selectedFeature) {
setSelectedFeature(null);
}
} else if (objectType === 'Polygon' || objectType === 'Point') {
} else if (objectType?.endsWith('Polygon') || objectType?.endsWith('Point')) {
let clickContext;
if (event.rightButton && Array.isArray(event.srcEvent?.point)) {
const {point} = event.srcEvent;
Expand All @@ -90,7 +90,7 @@ export function onClick(
};
}

if (objectType === 'Polygon') {
if (objectType?.endsWith('Polygon')) {
setSelectedFeature(info.object, clickContext);
} else {
// don't select points
Expand Down