diff --git a/frontend/orval.config.ts b/frontend/orval.config.ts index cef2a953..37c6da0e 100644 --- a/frontend/orval.config.ts +++ b/frontend/orval.config.ts @@ -38,6 +38,7 @@ module.exports = { 'Fishing-protection-level', 'Fishing-protection-level-stat', 'Layer', + 'Data-info', ], }, }, diff --git a/frontend/src/containers/data-tool/content/map/index.tsx b/frontend/src/containers/data-tool/content/map/index.tsx index e22f5bf4..cd65235d 100644 --- a/frontend/src/containers/data-tool/content/map/index.tsx +++ b/frontend/src/containers/data-tool/content/map/index.tsx @@ -12,6 +12,7 @@ import { useAtomValue, useSetAtom } from 'jotai'; import Map, { ZoomControls, Attributions, DrawControls, Drawing } from '@/components/map'; import SidebarContent from '@/components/sidebar-content'; // import Popup from '@/containers/map/popup'; +import LabelsManager from '@/containers/data-tool/content/map/labels-manager'; import LayersToolbox from '@/containers/data-tool/content/map/layers-toolbox'; import { useSyncMapSettings } from '@/containers/data-tool/content/map/sync-settings'; import { cn } from '@/lib/classnames'; @@ -192,6 +193,7 @@ const DataToolMap: React.FC = () => { > {/* */} + diff --git a/frontend/src/containers/data-tool/content/map/labels-manager/index.tsx b/frontend/src/containers/data-tool/content/map/labels-manager/index.tsx new file mode 100644 index 00000000..b1fb08bb --- /dev/null +++ b/frontend/src/containers/data-tool/content/map/labels-manager/index.tsx @@ -0,0 +1,41 @@ +import { useCallback, useEffect } from 'react'; + +import { useMap } from 'react-map-gl'; + +import { useSyncMapSettings } from '@/containers/data-tool/content/map/sync-settings'; + +const LABELS_LAYER_ID = 'country-label'; + +const LabelsManager = () => { + const { default: mapRef } = useMap(); + const [{ labels }] = useSyncMapSettings(); + + const toggleLabels = useCallback(() => { + if (!mapRef) return; + const map = mapRef.getMap(); + + map.setLayoutProperty(LABELS_LAYER_ID, 'visibility', labels ? 'visible' : 'none'); + }, [mapRef, labels]); + + const handleStyleLoad = useCallback(() => { + toggleLabels(); + }, [toggleLabels]); + + useEffect(() => { + if (!mapRef) return; + mapRef.on('style.load', handleStyleLoad); + + return () => { + mapRef.off('style.load', handleStyleLoad); + }; + }, [mapRef, handleStyleLoad]); + + useEffect(() => { + if (!mapRef) return; + toggleLabels(); + }, [mapRef, toggleLabels]); + + return null; +}; + +export default LabelsManager; diff --git a/frontend/src/containers/data-tool/content/map/layers-toolbox/layers-list/index.tsx b/frontend/src/containers/data-tool/content/map/layers-toolbox/layers-list/index.tsx index 8d7f300a..8023771b 100644 --- a/frontend/src/containers/data-tool/content/map/layers-toolbox/layers-list/index.tsx +++ b/frontend/src/containers/data-tool/content/map/layers-toolbox/layers-list/index.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { ComponentProps, useCallback } from 'react'; import { LuChevronDown, LuChevronUp } from 'react-icons/lu'; @@ -8,6 +8,7 @@ import { Switch } from '@/components/ui/switch'; import { useSyncMapLayers, useSyncMapLayerSettings, + useSyncMapSettings, } from '@/containers/data-tool/content/map/sync-settings'; import { useGetLayers } from '@/types/generated/layer'; import { LayerResponseDataObject } from '@/types/generated/strapi.schemas'; @@ -19,6 +20,7 @@ const TABS_ICONS_CLASSES = 'w-5 h-5 -translate-y-[2px]'; const LayersDropdown = (): JSX.Element => { const [activeLayers, setMapLayers] = useSyncMapLayers(); const [, setLayerSettings] = useSyncMapLayerSettings(); + const [{ labels }, setMapSettings] = useSyncMapSettings(); const layersQuery = useGetLayers( { @@ -51,6 +53,16 @@ const LayersDropdown = (): JSX.Element => { [activeLayers, setLayerSettings, setMapLayers] ); + const handleLabelsChange = useCallback( + (active: Parameters['onCheckedChange']>[0]) => { + setMapSettings((prev) => ({ + ...prev, + labels: active, + })); + }, + [setMapSettings] + ); + return (
@@ -83,7 +95,7 @@ const LayersDropdown = (): JSX.Element => { - + @@ -94,11 +106,7 @@ const LayersDropdown = (): JSX.Element => {
  • - {}} - /> + diff --git a/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/eez/index.tsx b/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/eez/index.tsx new file mode 100644 index 00000000..59b89a3f --- /dev/null +++ b/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/eez/index.tsx @@ -0,0 +1,60 @@ +import Icon from '@/components/ui/icon'; +import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; +import EEZIcon from '@/styles/icons/eez.svg?sprite'; +import InfoIcon from '@/styles/icons/info.svg?sprite'; +import SelectedEEZIcon from '@/styles/icons/selected-eez.svg?sprite'; +import SeveralEEZIcon from '@/styles/icons/several-eez.svg?sprite'; +import { useGetDataInfos } from '@/types/generated/data-info'; + +const ITEM_LIST_CLASSES = 'flex items-center space-x-2'; +const ICON_CLASSES = 'h-7 w-7'; + +const EEZLayerLegend = () => { + const EEZInfoQuery = useGetDataInfos( + { + filters: { + slug: 'eez-legend', + }, + }, + { + query: { + select: ({ data }) => data?.[0].attributes, + }, + } + ); + + return ( +
      +
    • + + EEZs +
    • +
    • + + Selected EEZ +
    • +
    • + +
      + + Area corresponding to more
      than one EEZ +
      + + + + + + +
      {EEZInfoQuery.data?.content}
      +
      +
      +
      +
      +
    • +
    + ); +}; + +export default EEZLayerLegend; diff --git a/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/index.tsx b/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/index.tsx index 7f37fe8a..52b83dbc 100644 --- a/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/index.tsx +++ b/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/index.tsx @@ -1,9 +1,16 @@ import { FC, useCallback } from 'react'; -import { ChevronUp, CircleDashed, Eye, EyeOff, MoveUp, X } from 'lucide-react'; +import { ChevronUp } from 'lucide-react'; +import { HiEye, HiEyeOff } from 'react-icons/hi'; -import { Accordion, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; import { Button } from '@/components/ui/button'; +import Icon from '@/components/ui/icon'; import { Label } from '@/components/ui/label'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Slider } from '@/components/ui/slider'; @@ -13,11 +20,15 @@ import { useSyncMapLayers, } from '@/containers/data-tool/content/map/sync-settings'; import { cn } from '@/lib/classnames'; +import ArrowDownIcon from '@/styles/icons/arrow-down.svg?sprite'; +import ArrowTopIcon from '@/styles/icons/arrow-top.svg?sprite'; +import CloseIcon from '@/styles/icons/close.svg?sprite'; +import OpacityIcon from '@/styles/icons/opacity.svg?sprite'; import { useGetLayers } from '@/types/generated/layer'; import { LayerResponseDataObject } from '@/types/generated/strapi.schemas'; -// import { LayerTyped } from '@/types/layers'; +import { LayerTyped } from '@/types/layers'; -// import LegendItems from './items'; +import LegendItem from './item'; const Legend: FC = () => { const [activeLayers, setMapLayers] = useSyncMapLayers(); @@ -140,7 +151,7 @@ const Legend: FC = () => { })} onValueChange={onToggleAccordion} > - {layersQuery.data?.map(({ id, attributes: { title } }, index) => { + {layersQuery.data?.map(({ id, attributes: { title, legend_config } }, index) => { const isFirst = index === 0; const isLast = index + 1 === layersQuery.data.length; @@ -175,7 +186,7 @@ const Legend: FC = () => {
    - + Move up @@ -200,7 +211,7 @@ const Legend: FC = () => { onClick={() => onMoveLayerDown(id)} > Move down - + Move down @@ -211,7 +222,7 @@ const Legend: FC = () => { @@ -237,8 +248,8 @@ const Legend: FC = () => { onClick={() => onToggleLayerVisibility(id, !isVisible)} > {isVisible ? 'Hide' : 'Show'} - {isVisible && } - {!isVisible && } + {isVisible && } + {!isVisible && } {isVisible ? 'Hide' : 'Show'} @@ -255,7 +266,7 @@ const Legend: FC = () => { }} > Remove - + Remove @@ -263,9 +274,9 @@ const Legend: FC = () => {
- {/* - - */} + + + ); })} diff --git a/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/items.tsx b/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/item.tsx similarity index 64% rename from frontend/src/containers/data-tool/content/map/layers-toolbox/legend/items.tsx rename to frontend/src/containers/data-tool/content/map/layers-toolbox/legend/item.tsx index 2159059e..23892110 100644 --- a/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/items.tsx +++ b/frontend/src/containers/data-tool/content/map/layers-toolbox/legend/item.tsx @@ -1,17 +1,36 @@ -import { FC } from 'react'; +import { FC, ReactElement, isValidElement, useMemo } from 'react'; -import { LayerTyped } from '@/types/layers'; +import { parseConfig } from '@/lib/json-converter'; +import { LayerTyped, LegendConfig } from '@/types/layers'; export interface LegendItemsProps { - items: LayerTyped['legend_config']; + config: LayerTyped['legend_config']; } -const LegendItems: FC = ({ items }) => { - switch (items.type) { +const LegendItem: FC = ({ config }) => { + const { type, items } = config; + + const LEGEND_ITEM_COMPONENT = useMemo(() => { + const l = parseConfig({ + config, + params_config: [], + settings: {}, + }); + + if (!l) return null; + + if (isValidElement(l)) { + return l; + } + + return null; + }, [config]); + + switch (type) { case 'basic': return (
    - {items.items.map(({ value, color }) => ( + {items.map(({ value, color }) => (
  • = ({ items }) => { return ( <>
      - {items.items.map(({ color }) => ( + {items.map(({ color }) => (
    • @@ -42,12 +61,12 @@ const LegendItems: FC = ({ items }) => {
      - {items.items.map(({ value }) => ( + {items.map(({ value }) => (
    • {value} @@ -63,14 +82,12 @@ const LegendItems: FC = ({ items }) => {
      i.color) - .join(',')})`, + backgroundImage: `linear-gradient(to right, ${items.map((i) => i.color).join(',')})`, }} />
        - {items.items + {items .filter(({ value }) => typeof value !== 'undefined' && value !== null) .map(({ value }) => (
      • @@ -82,8 +99,8 @@ const LegendItems: FC = ({ items }) => { ); default: - return null; + return LEGEND_ITEM_COMPONENT; } }; -export default LegendItems; +export default LegendItem; diff --git a/frontend/src/containers/data-tool/content/map/sync-settings.ts b/frontend/src/containers/data-tool/content/map/sync-settings.ts index e4f849c8..c16e0691 100644 --- a/frontend/src/containers/data-tool/content/map/sync-settings.ts +++ b/frontend/src/containers/data-tool/content/map/sync-settings.ts @@ -7,8 +7,10 @@ import { LayerSettings } from '@/types/layers'; const DEFAULT_SYNC_MAP_SETTINGS: { bbox: LngLatBoundsLike; + labels: boolean; } = { bbox: null, + labels: true, }; export const useSyncMapSettings = () => { diff --git a/frontend/src/lib/json-converter/index.ts b/frontend/src/lib/json-converter/index.ts index 5278e6fb..ce589aa4 100644 --- a/frontend/src/lib/json-converter/index.ts +++ b/frontend/src/lib/json-converter/index.ts @@ -8,6 +8,7 @@ import { JSONConfiguration, JSONConverter } from '@deck.gl/json/typed'; // LegendTypeChoropleth, // LegendTypeGradient, // } from '@/components/map/legend/item-types'; +import EEZLayerLegend from '@/containers/data-tool/content/map/layers-toolbox/legend/eez'; import EEZLayerPopup from '@/containers/data-tool/content/map/popup/eez'; import FUNCTIONS from '@/lib/utils'; import { ParamsConfig } from '@/types/layers'; @@ -24,6 +25,7 @@ export const JSON_CONFIGURATION = new JSONConfiguration({ enumerations: {}, reactComponents: { EEZLayerPopup, + EEZLayerLegend, // LegendTypeBasic, // LegendTypeChoropleth, // LegendTypeGradient, diff --git a/frontend/src/styles/icons/arrow-down.svg b/frontend/src/styles/icons/arrow-down.svg new file mode 100644 index 00000000..7bdae50a --- /dev/null +++ b/frontend/src/styles/icons/arrow-down.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/styles/icons/arrow-top.svg b/frontend/src/styles/icons/arrow-top.svg new file mode 100644 index 00000000..51f08c15 --- /dev/null +++ b/frontend/src/styles/icons/arrow-top.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/styles/icons/close.svg b/frontend/src/styles/icons/close.svg new file mode 100644 index 00000000..e0c139e6 --- /dev/null +++ b/frontend/src/styles/icons/close.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/styles/icons/eez.svg b/frontend/src/styles/icons/eez.svg new file mode 100644 index 00000000..60bf1bd8 --- /dev/null +++ b/frontend/src/styles/icons/eez.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/styles/icons/info.svg b/frontend/src/styles/icons/info.svg new file mode 100644 index 00000000..f33215a5 --- /dev/null +++ b/frontend/src/styles/icons/info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/styles/icons/opacity.svg b/frontend/src/styles/icons/opacity.svg new file mode 100644 index 00000000..232ee68c --- /dev/null +++ b/frontend/src/styles/icons/opacity.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/styles/icons/selected-eez.svg b/frontend/src/styles/icons/selected-eez.svg new file mode 100644 index 00000000..abcfcb88 --- /dev/null +++ b/frontend/src/styles/icons/selected-eez.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/styles/icons/several-eez.svg b/frontend/src/styles/icons/several-eez.svg new file mode 100644 index 00000000..4d260760 --- /dev/null +++ b/frontend/src/styles/icons/several-eez.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/types/generated/data-info.ts b/frontend/src/types/generated/data-info.ts new file mode 100644 index 00000000..e00e5e3b --- /dev/null +++ b/frontend/src/types/generated/data-info.ts @@ -0,0 +1,152 @@ +/** + * Generated by orval v6.18.1 🍺 + * Do not edit manually. + * DOCUMENTATION + * OpenAPI spec version: 1.0.0 + */ +import { useQuery } from '@tanstack/react-query'; +import type { + UseQueryOptions, + QueryFunction, + UseQueryResult, + QueryKey, +} from '@tanstack/react-query'; +import type { + DataInfoListResponse, + Error, + GetDataInfosParams, + DataInfoResponse, + GetDataInfosIdParams, +} from './strapi.schemas'; +import { API } from '../../services/api/index'; +import type { ErrorType } from '../../services/api/index'; + +// eslint-disable-next-line +type SecondParameter any> = T extends ( + config: any, + args: infer P +) => any + ? P + : never; + +export const getDataInfos = ( + params?: GetDataInfosParams, + options?: SecondParameter, + signal?: AbortSignal +) => { + return API({ url: `/data-infos`, method: 'get', params, signal }, options); +}; + +export const getGetDataInfosQueryKey = (params?: GetDataInfosParams) => { + return [`/data-infos`, ...(params ? [params] : [])] as const; +}; + +export const getGetDataInfosQueryOptions = < + TData = Awaited>, + TError = ErrorType +>( + params?: GetDataInfosParams, + options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetDataInfosQueryKey(params); + + const queryFn: QueryFunction>> = ({ signal }) => + getDataInfos(params, requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetDataInfosQueryResult = NonNullable>>; +export type GetDataInfosQueryError = ErrorType; + +export const useGetDataInfos = < + TData = Awaited>, + TError = ErrorType +>( + params?: GetDataInfosParams, + options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; + } +): UseQueryResult & { queryKey: QueryKey } => { + const queryOptions = getGetDataInfosQueryOptions(params, options); + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey; + + return query; +}; + +export const getDataInfosId = ( + id: number, + params?: GetDataInfosIdParams, + options?: SecondParameter, + signal?: AbortSignal +) => { + return API( + { url: `/data-infos/${id}`, method: 'get', params, signal }, + options + ); +}; + +export const getGetDataInfosIdQueryKey = (id: number, params?: GetDataInfosIdParams) => { + return [`/data-infos/${id}`, ...(params ? [params] : [])] as const; +}; + +export const getGetDataInfosIdQueryOptions = < + TData = Awaited>, + TError = ErrorType +>( + id: number, + params?: GetDataInfosIdParams, + options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetDataInfosIdQueryKey(id, params); + + const queryFn: QueryFunction>> = ({ signal }) => + getDataInfosId(id, params, requestOptions, signal); + + return { queryKey, queryFn, enabled: !!id, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetDataInfosIdQueryResult = NonNullable>>; +export type GetDataInfosIdQueryError = ErrorType; + +export const useGetDataInfosId = < + TData = Awaited>, + TError = ErrorType +>( + id: number, + params?: GetDataInfosIdParams, + options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; + } +): UseQueryResult & { queryKey: QueryKey } => { + const queryOptions = getGetDataInfosIdQueryOptions(id, params, options); + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey; + + return query; +}; diff --git a/frontend/src/types/generated/strapi.schemas.ts b/frontend/src/types/generated/strapi.schemas.ts index ecb2eaee..6579283b 100644 --- a/frontend/src/types/generated/strapi.schemas.ts +++ b/frontend/src/types/generated/strapi.schemas.ts @@ -604,6 +604,56 @@ export type GetFishingProtectionLevelsParams = { locale?: string; }; +export type GetDataInfosIdParams = { + /** + * Relations to return + */ + populate?: string; +}; + +export type GetDataInfosParams = { + /** + * Sort by attributes ascending (asc) or descending (desc) + */ + sort?: string; + /** + * Return page/pageSize (default: true) + */ + 'pagination[withCount]'?: boolean; + /** + * Page number (default: 0) + */ + 'pagination[page]'?: number; + /** + * Page size (default: 25) + */ + 'pagination[pageSize]'?: number; + /** + * Offset value (default: 0) + */ + 'pagination[start]'?: number; + /** + * Number of entities to return (default: 25) + */ + 'pagination[limit]'?: number; + /** + * Fields to return (ex: title,author) + */ + fields?: string; + /** + * Relations to return + */ + populate?: string; + /** + * Filters to apply + */ + filters?: { [key: string]: any }; + /** + * Locale to apply + */ + locale?: string; +}; + /** * every controller of the api */