Skip to content
Merged
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
10 changes: 10 additions & 0 deletions src/features/search/atoms/highlightedGeometry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { atom } from '@reatom/framework';
import type { Feature, FeatureCollection } from 'geojson';

export const searchHighlightedGeometryAtom = atom<FeatureCollection | Feature>(
{
type: 'FeatureCollection',
features: [],
},
'searchHighlightedGeometryAtom',
);
34 changes: 29 additions & 5 deletions src/features/search/componets/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SelectItem } from '@konturio/ui-kit';
import { useAction, useAtom } from '@reatom/npm-react';
import cn from 'clsx';
import { forwardRef } from 'react';
import { forwardRef, useEffect } from 'react';
import { searchLocationsAtom } from '~features/search/searchLocationAtoms';
import {
itemSelectAction,
Expand All @@ -16,6 +16,9 @@ import {
MCDASuggestionAtom,
} from '~features/search/searchMcdaAtoms';
import { SearchInput } from '~components/Search/SearchInput/SearchInput';
import { searchHighlightedGeometryAtom } from '../../atoms/highlightedGeometry';
import type { Feature } from 'geojson';
import { EMPTY_HIGHLIGHT } from '../../constants';
import { useSearchMenu } from '~utils/hooks/useSearchMenu';
import style from './SearchBar.module.css';
import type { AggregatedSearchItem } from '~features/search/searchAtoms';
Expand All @@ -33,19 +36,27 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
? i18n.t('search.input_placeholder_mcda')
: i18n.t('search.input_placeholder');

const search = useAction(searchAction);
const itemSelectActionFn = useAction(itemSelectAction);
const reset = useAction(resetSearchAction);
const search = useAction(searchAction);
const itemSelectActionFn = useAction(itemSelectAction);
const reset = useAction(resetSearchAction);
const setHighlightedGeometry = useAction(searchHighlightedGeometryAtom);

const itemSelect = (item: AggregatedSearchItem) => {
itemSelectActionFn(item);
setHighlightedGeometry(EMPTY_HIGHLIGHT);
onItemSelect?.();
};

const [{ error, loading, data }] = useAtom(searchLocationsAtom);
const emptyLocations = data ? data.length === 0 : false;
const [mcdaSearchStatus] = useAtom(MCDASuggestionAtom);
const [aggregatedResults] = useAtom(aggregatedSearchAtom);
const [aggregatedResults] = useAtom(aggregatedSearchAtom);

useEffect(() => {
return () => {
setHighlightedGeometry(EMPTY_HIGHLIGHT);
};
}, [setHighlightedGeometry]);

const renderError = () => (
<SelectItem
Expand Down Expand Up @@ -91,6 +102,11 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
className={style.listItem}
itemProps={{
onClick: () => handleItemSelect(item),
onMouseEnter: () =>
item.geometry &&
setHighlightedGeometry(item as Feature),
onMouseLeave: () =>
setHighlightedGeometry(EMPTY_HIGHLIGHT),
role: 'option',
}}
/>
Expand All @@ -108,6 +124,10 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
highlighted={highlightedIndex === index}
itemProps={{
onClick: () => handleItemSelect(item),
onMouseEnter: () =>
setHighlightedGeometry(EMPTY_HIGHLIGHT),
onMouseLeave: () =>
setHighlightedGeometry(EMPTY_HIGHLIGHT),
role: 'option',
}}
/>
Expand Down Expand Up @@ -147,6 +167,10 @@ export const SearchBar = forwardRef<HTMLInputElement, SearchBarProps>(
emptyLocations,
});

useEffect(() => {
if (!isMenuOpen) setHighlightedGeometry(EMPTY_HIGHLIGHT);
}, [isMenuOpen, setHighlightedGeometry]);

return (
<>
<div className={cn(style.searchBar, searchBarClass)} ref={searchBarRef}>
Expand Down
8 changes: 8 additions & 0 deletions src/features/search/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { FeatureCollection } from 'geojson';

export const SEARCH_HIGHLIGHT_LAYER_ID = 'search-highlight';
export const SEARCH_HIGHLIGHT_COLOR = '#000000';
export const EMPTY_HIGHLIGHT: FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
8 changes: 8 additions & 0 deletions src/features/search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { i18n } from '~core/localization';
import { useShortPanelState } from '~utils/hooks/useShortPanelState';
import { useAutoCollapsePanel } from '~utils/hooks/useAutoCollapsePanel';
import { SearchBar } from '~features/search/componets/SearchBar/SearchBar';
import { initSearchHighlightLayer } from './initSearchHighlightLayer';
import s from './styles.module.css';

export function Search() {
Expand All @@ -18,6 +19,13 @@ export function Search() {
const inputRef = useRef<HTMLInputElement>(null);
const isMobile = useMediaQuery(IS_MOBILE_QUERY);

useEffect(() => {
const destroyHighlightLayer = initSearchHighlightLayer();
return () => {
destroyHighlightLayer?.();
};
}, []);

useEffect(() => {
if (isOpen && isMobile) {
inputRef?.current?.focus(); // triggers phone keyboard
Expand Down
74 changes: 74 additions & 0 deletions src/features/search/initSearchHighlightLayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { currentMapAtom } from '~core/shared_state';
import { store } from '~core/store/store';
import type { LogicalLayerState } from '~core/logical_layers/types/logicalLayer';
import type { Feature, FeatureCollection } from 'geojson';
import {
SEARCH_HIGHLIGHT_LAYER_ID,
SEARCH_HIGHLIGHT_COLOR,
} from './constants';
import { searchHighlightedGeometryAtom } from './atoms/highlightedGeometry';
import { SearchHighlightRenderer } from './renderers/SearchHighlightRenderer';

let cleanUp: (() => void) | null = null;

export function initSearchHighlightLayer() {
if (cleanUp) return cleanUp;

const ctx = store.v3ctx;
const map = ctx.get(currentMapAtom.v3atom);
if (!map) return () => {};

const sourceId = `${SEARCH_HIGHLIGHT_LAYER_ID}-source`;
const renderer = new SearchHighlightRenderer({
layerId: SEARCH_HIGHLIGHT_LAYER_ID,
sourceId,
color: SEARCH_HIGHLIGHT_COLOR,
});

const state: LogicalLayerState = {
id: SEARCH_HIGHLIGHT_LAYER_ID,
isDownloadable: false,
isVisible: true,
isLoading: false,
isEnabled: true,
isEditable: false,
isMounted: true,
source: {
id: sourceId,
source: { type: 'geojson', data: { type: 'FeatureCollection', features: [] } },
},
legend: null,
meta: null,
settings: null,
error: null,
contextMenu: null,
style: null,
editor: null,
};

renderer.willMount({ map, state });

const unsubscribe = ctx.subscribe(
searchHighlightedGeometryAtom,
(geometry: FeatureCollection | Feature) => {
renderer.willSourceUpdate({
map,
state: {
...state,
source: {
id: sourceId,
source: { type: 'geojson', data: geometry },
},
},
});
},
);

cleanUp = () => {
unsubscribe();
renderer.willUnMount({ map });
cleanUp = null;
};

return cleanUp;
}
12 changes: 12 additions & 0 deletions src/features/search/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Search feature

This feature provides a search bar to find locations or MCDA analysis suggestions.
Selected locations become the focused area on the map.
While hovering over location results the geometry is highlighted on the map with a black outline.

## Main parts
- `SearchBar` - UI component with dropdown results.
- `searchLocationAtoms.ts` - handles locations fetching and selection.
- `searchAtoms.ts` - aggregates MCDA and location results.
- `atoms/highlightedGeometry.ts` - stores geometry highlighted on hover.
- `initSearchHighlightLayer` - registers temporary map layer for hovered geometries.
3 changes: 3 additions & 0 deletions src/features/search/renderers/SearchHighlightRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { BoundarySelectorRenderer } from '~features/boundary_selector/renderers/BoundarySelectorRenderer';

export class SearchHighlightRenderer extends BoundarySelectorRenderer {}
3 changes: 3 additions & 0 deletions src/features/search/searchAtoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
selectMCDAItemAction,
isMCDASearchEnabled,
} from '~features/search/searchMcdaAtoms';
import { searchHighlightedGeometryAtom } from './atoms/highlightedGeometry';
import { EMPTY_HIGHLIGHT } from './constants';
import type { LocationProperties } from '~core/api/search';
import type { Geometry } from 'geojson';
import type { MCDAConfig } from '~core/logical_layers/renderers/stylesConfigs/mcda/types';
Expand Down Expand Up @@ -63,6 +65,7 @@ export const itemSelectAction = action((ctx, item: AggregatedSearchItem) => {
} else if (item.source === 'mcda') {
selectMCDAItemAction(ctx);
}
searchHighlightedGeometryAtom(ctx, EMPTY_HIGHLIGHT);
});

export const resetSearchAction = action((ctx) => {
Expand Down
13 changes: 12 additions & 1 deletion src/features/search/searchLocationAtoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
withStatusesAtom,
} from '@reatom/framework';
import { getLocations } from '~core/api/search';
import { setCurrentMapBbox } from '~core/shared_state/currentMapPosition';
import {
setCurrentMapBbox,
focusOnGeometry,
} from '~core/shared_state/currentMapPosition';
import { focusedGeometryAtom } from '~core/focused_geometry/model';

export const fetchLocationsAsyncResource = reatomAsync(
(ctx, query: string) => getLocations(query, ctx.controller),
Expand All @@ -28,6 +32,13 @@ export const searchLocationsAtom = atom((ctx) => {
export const selectLocationItemAction = action((ctx, item) => {
const bbox = item.properties.bbox;
setCurrentMapBbox(ctx, bbox);
if (item.geometry) {
focusedGeometryAtom.setFocusedGeometry.v3action(ctx, {
source: { type: 'custom' },
geometry: item,
});
focusOnGeometry(ctx, item);
}
});

export const resetLocationSearchAction = action((ctx) => {
Expand Down
Loading