Skip to content

Commit

Permalink
Merge pull request #78 from Vizzuality/SKY30-129-fe-implement-the-too…
Browse files Browse the repository at this point in the history
…ltip-for

[SKY30-129]: tooltips for remaining layers
  • Loading branch information
agnlez authored Nov 27, 2023
2 parents 91ba860 + d5be31d commit 24e2a6e
Show file tree
Hide file tree
Showing 9 changed files with 420 additions and 28 deletions.
12 changes: 8 additions & 4 deletions frontend/src/containers/data-tool/content/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const DataToolMap: React.FC = () => {
const isSidebarOpen = useAtomValue(sidebarAtom);
const setPopup = useSetAtom(popupAtom);
const { locationCode } = useParams();
const hoveredPolygonId = useRef<string | number | null>(null);
const locationBbox = useAtomValue(bboxLocation);
const hoveredPolygonId = useRef<Parameters<typeof map.setFeatureState>[0] | null>(null);

const locationsQuery = useGetLocations(
{},
Expand Down Expand Up @@ -104,7 +104,7 @@ const DataToolMap: React.FC = () => {
map.setFeatureState(
{
source: e.features?.[0].source,
id: hoveredPolygonId.current,
id: hoveredPolygonId.current.id,
sourceLayer: e.features?.[0].sourceLayer,
},
{ hover: false }
Expand All @@ -119,7 +119,7 @@ const DataToolMap: React.FC = () => {
{ hover: true }
);

hoveredPolygonId.current = e.features[0].id;
hoveredPolygonId.current = e.features[0];
}
},
[map, hoveredPolygonId]
Expand All @@ -129,7 +129,11 @@ const DataToolMap: React.FC = () => {
if (hoveredPolygonId.current !== null) {
map.setFeatureState(
// ? not a fan of harcoding the sources here, but there is no other way to find out the source
{ source: 'ezz-source', id: hoveredPolygonId.current, sourceLayer: 'eez_v11' },
{
source: hoveredPolygonId.current.source,
id: hoveredPolygonId.current.id,
sourceLayer: hoveredPolygonId.current.sourceLayer,
},
{ hover: false }
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const LayersDropdown = (): JSX.Element => {
query: {
select: ({ data }) => data,
placeholderData: { data: [] },
keepPreviousData: true,
},
}
);
Expand Down
35 changes: 24 additions & 11 deletions frontend/src/containers/data-tool/content/map/popup/eez/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,55 +77,68 @@ const EEZLayerPopup = ({ locationId }) => {
return DATA_REF.current;
}, [popup, source, layersInteractiveIds, map, rendered]);

const locationQuery = useGetLocations(
const locationsQuery = useGetLocations(
{
filters: {
code: DATA?.ISO_SOV1,
code: {
$in: ['GLOB', DATA?.ISO_SOV1],
},
},
},
{
query: {
enabled: !!DATA?.ISO_SOV1,
select: ({ data }) => data[0],
select: ({ data }) => data,
},
}
);

const worldLocation = locationsQuery.data?.find(({ attributes: { code } }) => code === 'GLOB');
const currentLocation = locationsQuery.data?.find(({ attributes: { code } }) => code !== 'GLOB');

// handle renderer
const handleMapRender = useCallback(() => {
setRendered(!!map?.loaded() && !!map?.areTilesLoaded());
setRendered(map?.loaded() && map?.areTilesLoaded());
}, [map]);

useEffect(() => {
map?.on('render', handleMapRender);

setRendered(map?.loaded() && map?.areTilesLoaded());

return () => {
map?.off('render', handleMapRender);
};
}, [map, handleMapRender]);

if (!DATA) return null;

const coverage =
currentLocation?.attributes?.totalMarineArea / worldLocation?.attributes?.totalMarineArea;

return (
<>
<div className="space-y-2 p-4">
<div className="space-y-2">
<h3 className="text-xl font-semibold">{DATA?.GEONAME}</h3>
{locationQuery.isFetching && !locationQuery.isFetched && (
{locationsQuery.isFetching && !locationsQuery.isFetched && (
<span className="text-sm">Loading...</span>
)}
{locationQuery.isFetched && !locationQuery.data && (
{locationsQuery.isFetched && !locationsQuery.data && (
<span className="text-sm">No data available</span>
)}
{locationQuery.isFetched && locationQuery.data && (
{locationsQuery.isFetched && locationsQuery.data && (
<>
<dl className="space-y-2">
<dt className={TERMS_CLASSES}>Marine conservation coverage</dt>
<dd className="font-mono text-6xl tracking-tighter text-blue">
39<span className="text-xl">%</span>
{format({
value: coverage,
id: 'formatPercentage',
})}
</dd>
<dd className="font-mono text-xl text-blue">
{format({
value: locationQuery.data?.attributes?.totalMarineArea,
value: currentLocation?.attributes?.totalMarineArea,
id: 'formatKM',
})}
Km<sup>2</sup>
Expand All @@ -135,7 +148,7 @@ const EEZLayerPopup = ({ locationId }) => {
className="block border border-black p-4 text-center font-mono uppercase"
href={`${
PAGES.dataTool
}/${locationQuery.data?.attributes?.code.toUpperCase()}?${searchParams.toString()}`}
}/${currentLocation?.attributes?.code.toUpperCase()}?${searchParams.toString()}`}
>
Open country insights
</Link>
Expand Down
119 changes: 119 additions & 0 deletions frontend/src/containers/data-tool/content/map/popup/generic/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { useMap } from 'react-map-gl';

import type { Feature } from 'geojson';
import { useAtomValue } from 'jotai';

import { layersInteractiveIdsAtom, popupAtom } from '@/containers/data-tool/store';
import { format } from '@/lib/utils/formats';
import { useGetLayersId } from '@/types/generated/layer';
import { LayerTyped, InteractionConfig } from '@/types/layers';

const TERMS_CLASSES = 'font-mono font-bold uppercase';

const GenericPopup = ({
locationId,
...restConfig
}: InteractionConfig & { locationId: number }) => {
const [rendered, setRendered] = useState(false);
const DATA_REF = useRef<Feature['properties'] | undefined>();
const { default: map } = useMap();
const { events } = restConfig;

const popup = useAtomValue(popupAtom);
const layersInteractiveIds = useAtomValue(layersInteractiveIdsAtom);

const layerQuery = useGetLayersId(
locationId,
{
populate: 'metadata',
},
{
query: {
select: ({ data }) => ({
source: (data.attributes as LayerTyped).config?.source,
click: (data.attributes as LayerTyped)?.interaction_config?.events.find(
(ev) => ev.type === 'click'
),
}),
},
}
);

const { source } = layerQuery.data;

const DATA = useMemo(() => {
if (source?.type === 'vector' && rendered && popup && map) {
const point = map.project(popup.lngLat);

// check if the point is outside the canvas
if (
point.x < 0 ||
point.x > map.getCanvas().width ||
point.y < 0 ||
point.y > map.getCanvas().height
) {
return DATA_REF.current;
}

const query = map.queryRenderedFeatures(point, {
layers: layersInteractiveIds,
});

const d = query.find((d) => {
return d.source === source.id;
})?.properties;

DATA_REF.current = d;

if (d) {
return DATA_REF.current;
}
}

return DATA_REF.current;
}, [popup, source, layersInteractiveIds, map, rendered]);

// handle renderer
const handleMapRender = useCallback(() => {
setRendered(map?.loaded() && map?.areTilesLoaded());
}, [map]);

useEffect(() => {
map?.on('render', handleMapRender);

setRendered(map?.loaded() && map?.areTilesLoaded());

return () => {
map?.off('render', handleMapRender);
};
}, [map, handleMapRender]);

if (!DATA) return null;

const values = events.find((ev) => ev.type === 'click')?.values;
const name = values?.find((v) => v.label === 'Name');
const restValues = values?.filter((v) => v.label !== 'Name') ?? [];

return (
<>
<div className="space-y-2">
{name && <h3 className="text-xl font-semibold line-clamp-2">{DATA?.[name.key]}</h3>}
<dl className="space-y-2">
{restValues.map(({ key, label, format: customFormat }) => (
<div key={key}>
<dt className={TERMS_CLASSES}>{label}</dt>
<dd className="font-mono first-letter:uppercase">
{customFormat && format({ value: DATA[key], ...customFormat })}
{!customFormat && DATA[key]}
</dd>
</div>
))}
</dl>
</div>
</>
);
};

export default GenericPopup;
108 changes: 98 additions & 10 deletions frontend/src/containers/data-tool/content/map/popup/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,123 @@
import { useCallback, useEffect, useState } from 'react';

import { Popup } from 'react-map-gl';

import { useAtomValue, useSetAtom } from 'jotai';

import Icon from '@/components/ui/icon';
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from '@/components/ui/select';
import PopupItem from '@/containers/data-tool/content/map/popup/item';
import { layersInteractiveAtom, popupAtom } from '@/containers/data-tool/store';
import CloseIcon from '@/styles/icons/close.svg?sprite';
import { useGetLayers } from '@/types/generated/layer';

import { useSyncMapLayers } from '../sync-settings';

const PopupContainer = () => {
const popup = useAtomValue(popupAtom);
const layersInteractive = useAtomValue(layersInteractiveAtom);
const lys = [...layersInteractive].reverse();
const [syncedLayers] = useSyncMapLayers();

const [selectedLayerId, setSelectedLayerId] = useState<number | null>(null);

const setPopup = useSetAtom(popupAtom);

if (!Object.keys(popup).length) return null;
const availableSources = popup?.features?.map(({ source }) => source);

const { data: layersInteractiveData } = useGetLayers(
{
filters: {
id: {
$in: layersInteractive,
},
},
},
{
query: {
enabled: layersInteractive.length > 1,
select: ({ data }) =>
data
.filter(
({
attributes: {
config: {
// @ts-expect-error will check later
source: { id: sourceId },
},
},
}) => availableSources?.includes(sourceId)
)
.map(({ attributes: { title: label }, id: value }) => ({
label,
value: value.toString(),
}))
.sort((a, b) =>
syncedLayers.indexOf(Number(a.value)) > syncedLayers.indexOf(Number(b.value)) ? 1 : -1
),
},
}
);

const closePopup = useCallback(() => {
setPopup({});
}, [setPopup]);

useEffect(() => {
if (!layersInteractive.length) {
closePopup();
}
}, [layersInteractive, closePopup]);

useEffect(() => {
if (layersInteractiveData?.[0]?.value) {
setSelectedLayerId(Number(layersInteractiveData[0].value));
}
}, [layersInteractiveData]);

if (!Object.keys(popup).length || !popup?.features?.length) return null;

return (
<Popup
latitude={popup.lngLat.lat}
longitude={popup.lngLat.lng}
closeOnClick={false}
closeButton={false}
style={{
padding: 0,
}}
maxWidth="300px"
onClose={() => setPopup({})}
onClose={closePopup}
className="min-w-[250px]"
>
<div className="">
{lys.map((id) => (
<PopupItem key={id} id={id} />
))}
<div className="space-y-2 p-4">
<div className="flex justify-end">
<button onClick={closePopup}>
<Icon icon={CloseIcon} className="h-3 w-3 fill-black" />
</button>
</div>
{availableSources.length > 1 && (
<Select
onValueChange={(v) => {
setSelectedLayerId(+v);
}}
defaultValue={layersInteractiveData?.[0].value}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{layersInteractiveData?.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{selectedLayerId && <PopupItem id={selectedLayerId} />}
</div>
</Popup>
);
Expand Down
Loading

0 comments on commit 24e2a6e

Please sign in to comment.