diff --git a/assets/@types/app.ts b/assets/@types/app.ts index 60cb78b2..f12c65cb 100644 --- a/assets/@types/app.ts +++ b/assets/@types/app.ts @@ -76,6 +76,7 @@ export enum DatasheetDocumentTypeEnum { export type DatasheetDetailed = Datasheet & { vector_db_list: VectorDb[] | undefined; pyramid_vector_list: PyramidVector[] | undefined; + pyramid_raster_list: PyramidRaster[] | undefined; upload_list: Upload[] | undefined; service_list: Service[] | undefined; }; @@ -112,6 +113,14 @@ export type PyramidVector = StoredData & { }; }; +/** stored_data (donnée stockée) du type ROK4-PYRAMID-VECTOR */ +export type PyramidRaster = StoredData & { + type: StoredDataPrivateDetailResponseDtoTypeEnum.ROK4PYRAMIDRASTER; + tags: { + datasheet_name?: string; + }; +}; + /** upload (livraison) */ export type Upload = UploadPrivateDetailResponseDto & { tags: { diff --git a/assets/components/Utils/ZoomRange.tsx b/assets/components/Utils/ZoomRange.tsx index 7845faa6..5a12bc74 100644 --- a/assets/components/Utils/ZoomRange.tsx +++ b/assets/components/Utils/ZoomRange.tsx @@ -140,24 +140,24 @@ const ZoomRange: FC = (props) => {
{(mode === "both" || mode === "top") && ( -
+
)} {mode !== "both" && ( -
+
- {overlayContent &&
{overlayContent}
} + {overlayContent &&
{overlayContent}
}
)} {(mode === "both" || mode === "bottom") && ( -
+
)} @@ -248,6 +248,5 @@ const useStyles = tss.withName(ZoomRange.displayName).create(() => ({ falseMapOverlayContent: { backgroundColor: fr.colors.decisions.background.default.grey.default, color: fr.colors.decisions.text.default.grey.default, - padding: fr.spacing("2v"), }, })); diff --git a/assets/entrepot/api/index.ts b/assets/entrepot/api/index.ts index a61d3d07..0dd0249b 100644 --- a/assets/entrepot/api/index.ts +++ b/assets/entrepot/api/index.ts @@ -10,6 +10,7 @@ import user from "./user"; import wfs from "./wfs"; import wmsVector from "./wms-vector"; import pyramidVector from "./pyramidVector"; +import pyramidRaster from "./pyramidRaster"; import service from "./service"; import epsg from "./epsg"; import annexe from "./annexe"; @@ -32,6 +33,7 @@ const api = { wfs, wmsVector, pyramidVector, + pyramidRaster, service, annexe, style, diff --git a/assets/entrepot/api/pyramidRaster.ts b/assets/entrepot/api/pyramidRaster.ts new file mode 100644 index 00000000..d3c8e08d --- /dev/null +++ b/assets/entrepot/api/pyramidRaster.ts @@ -0,0 +1,22 @@ +import SymfonyRouting from "../../modules/Routing"; +import { jsonFetch } from "../../modules/jsonFetch"; +import type { PyramidRaster } from "../../@types/app"; + +const add = (datastoreId: string, formData: FormData | object) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_pyramid_raster_add", { datastoreId }); + return jsonFetch( + url, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }, + formData + ); +}; + +export default { + add, +}; diff --git a/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/DatasetListTab.tsx b/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/DatasetListTab.tsx index 9e8786f3..e2da4876 100644 --- a/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/DatasetListTab.tsx +++ b/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/DatasetListTab.tsx @@ -8,6 +8,7 @@ import { routes } from "../../../../../router/router"; import PyramidVectorList from "./PyramidVectorList/PyramidVectorList"; import UnfinishedUploadList from "./UnfinishedUploadList"; import VectorDbList from "./VectorDbList/VectorDbList"; +import PyramidRasterList from "./PyramidRasterList/PyramidRasterList"; type DataListTabProps = { datastoreId: string; @@ -55,6 +56,11 @@ const DatasetListTab: FC = ({ datastoreId, datasheet }) => {
+
+
+ +
+
); }; diff --git a/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterList.tsx b/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterList.tsx new file mode 100644 index 00000000..8e4abac1 --- /dev/null +++ b/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterList.tsx @@ -0,0 +1,32 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import { FC, memo } from "react"; +import { symToStr } from "tsafe/symToStr"; + +import { PyramidRaster } from "../../../../../../@types/app"; +import PyramidRasterListItem from "./PyramidRasterListItem"; + +type PyramidRasterListProps = { + datasheetName: string; + datastoreId: string; + pyramidList: PyramidRaster[] | undefined; +}; + +const PyramidRasterList: FC = ({ datasheetName, datastoreId, pyramidList }) => { + return ( + <> +
+
+ +  Pyramides de tuiles raster ({pyramidList?.length}) +
+
+ {pyramidList?.map((pyramid) => ( + + ))} + + ); +}; + +PyramidRasterList.displayName = symToStr({ PyramidRasterList }); + +export default memo(PyramidRasterList); diff --git a/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterListItem.tsx b/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterListItem.tsx new file mode 100644 index 00000000..7ecf90f1 --- /dev/null +++ b/assets/entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterListItem.tsx @@ -0,0 +1,221 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import Badge from "@codegouvfr/react-dsfr/Badge"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { FC, memo, useMemo } from "react"; +import { createPortal } from "react-dom"; + +import { PyramidRaster, StoredDataStatusEnum } from "../../../../../../@types/app"; +import StoredDataStatusBadge from "../../../../../../components/Utils/Badges/StoredDataStatusBadge"; +import LoadingIcon from "../../../../../../components/Utils/LoadingIcon"; +import LoadingText from "../../../../../../components/Utils/LoadingText"; +import MenuList from "../../../../../../components/Utils/MenuList"; +import Wait from "../../../../../../components/Utils/Wait"; +import useToggle from "../../../../../../hooks/useToggle"; +import { Translations, declareComponentKeys, getTranslation, useTranslation } from "../../../../../../i18n/i18n"; +import RQKeys from "../../../../../../modules/entrepot/RQKeys"; +import { routes } from "../../../../../../router/router"; +import { formatDateFromISO, offeringTypeDisplayName } from "../../../../../../utils"; +import api from "../../../../../api"; +// import PyramidRasterDesc from "./PyramidRasterDesc"; + +type PyramidRasterListItemProps = { + datasheetName: string; + pyramid: PyramidRaster; + datastoreId: string; +}; + +const { t: tCommon } = getTranslation("Common"); + +const PyramidRasterListItem: FC = ({ datasheetName, datastoreId, pyramid }) => { + const { t } = useTranslation({ PyramidRasterListItem }); + + const [showDescription, toggleShowDescription] = useToggle(false); + + const dataUsesQuery = useQuery({ + queryKey: RQKeys.datastore_stored_data_uses(datastoreId, pyramid._id), + queryFn: ({ signal }) => api.storedData.getUses(datastoreId, pyramid._id, { signal }), + staleTime: 600000, + enabled: showDescription, + }); + + /* Suppression de la pyramide */ + const queryClient = useQueryClient(); + + const deletePyramidMutation = useMutation({ + mutationFn: () => api.storedData.remove(datastoreId, pyramid._id), + onSuccess() { + if (datasheetName) { + queryClient.invalidateQueries({ queryKey: RQKeys.datastore_datasheet(datastoreId, datasheetName) }); + } + }, + }); + + const confirmRemovePyramidModal = useMemo( + () => + createModal({ + id: `confirm-delete-pyramid-${pyramid._id}`, + isOpenedByDefault: false, + }), + [pyramid._id] + ); + + return ( + <> +
+
+
+
+
+
+ +
+
+

{pyramid?.last_event?.date && formatDateFromISO(pyramid?.last_event?.date)}

+ + + confirmRemovePyramidModal.open(), + }, + ]} + /> +
+
+
+ {/* {showDescription && } */} +
+ {deletePyramidMutation.error && ( + + )} + {deletePyramidMutation.isPending && ( + +
+
+ +
{tCommon("removing")}
+
+
+
+ )} + {createPortal( + deletePyramidMutation.mutate(), + priority: "primary", + }, + ]} + > + {dataUsesQuery.isFetching && } + + {dataUsesQuery.data?.offerings_list && dataUsesQuery.data?.offerings_list?.length > 0 && ( +
+

{t("following_services_deleted")}

+
+
    + {dataUsesQuery.data?.offerings_list.map((offering, i) => ( +
  • + {offering.layer_name} + {offeringTypeDisplayName(offering.type)} +
  • + ))} +
+
+
+ )} +
, + document.body + )} + + ); +}; + +export default memo(PyramidRasterListItem); + +// traductions +export const { i18n } = declareComponentKeys< + | "show_linked_datas" + | "other_actions" + | "show_details" + | "publish_tms_service" + | { K: "confirm_delete_modal.title"; P: { pyramidName: string }; R: string } + | "following_services_deleted" + | { K: "error_deleting"; P: { pyramidName: string }; R: string } +>()({ + PyramidRasterListItem, +}); + +export const PyramidRasterListItemFrTranslations: Translations<"fr">["PyramidRasterListItem"] = { + show_linked_datas: "Voir les données liées", + other_actions: "Autres actions", + show_details: "Voir les détails", + publish_tms_service: "Publier le service TMS", + "confirm_delete_modal.title": ({ pyramidName }) => `Êtes-vous sûr de vouloir supprimer la pyramide ${pyramidName} ?`, + following_services_deleted: "Les services suivants seront aussi supprimés :", + error_deleting: ({ pyramidName }) => `La suppression de la pyramide ${pyramidName} a échoué`, +}; + +export const PyramidRasterListItemEnTranslations: Translations<"en">["PyramidRasterListItem"] = { + show_linked_datas: "Show linked datas", + other_actions: "Other actions", + show_details: "Show details", + publish_tms_service: "Publish TMS service", + "confirm_delete_modal.title": ({ pyramidName }) => `Are you sure you want to delete pyramid ${pyramidName} ?`, + following_services_deleted: "The following services will be deleted :", + error_deleting: ({ pyramidName }) => `Deleting ${pyramidName} pyramid failed`, +}; diff --git a/assets/entrepot/pages/datasheet/DatasheetView/DatasheetView.tsx b/assets/entrepot/pages/datasheet/DatasheetView/DatasheetView.tsx index 01d9e27a..14f03121 100644 --- a/assets/entrepot/pages/datasheet/DatasheetView/DatasheetView.tsx +++ b/assets/entrepot/pages/datasheet/DatasheetView/DatasheetView.tsx @@ -166,7 +166,10 @@ const DatasheetView: FC = ({ datastoreId, datasheetName }) = }, { label: t("tab_label.datasets", { - num: (datasheetQuery.data?.vector_db_list?.length || 0) + (datasheetQuery.data?.pyramid_vector_list?.length || 0), + num: + (datasheetQuery.data?.vector_db_list?.length || 0) + + (datasheetQuery.data?.pyramid_vector_list?.length || 0) + + (datasheetQuery.data?.pyramid_raster_list?.length || 0), }), tabId: DatasheetViewActiveTabEnum.Dataset, }, diff --git a/assets/entrepot/pages/service/wms-raster-wmts/PyramidRasterGenerateForm.tsx b/assets/entrepot/pages/service/wms-raster-wmts/PyramidRasterGenerateForm.tsx index 983e230f..d9f90d89 100644 --- a/assets/entrepot/pages/service/wms-raster-wmts/PyramidRasterGenerateForm.tsx +++ b/assets/entrepot/pages/service/wms-raster-wmts/PyramidRasterGenerateForm.tsx @@ -5,14 +5,19 @@ import Button from "@codegouvfr/react-dsfr/Button"; import ButtonsGroup from "@codegouvfr/react-dsfr/ButtonsGroup"; import Input from "@codegouvfr/react-dsfr/Input"; import Stepper from "@codegouvfr/react-dsfr/Stepper"; -import { useQuery } from "@tanstack/react-query"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { declareComponentKeys } from "i18nifty"; import { FC, useCallback, useState } from "react"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; -import type { Service } from "../../../../@types/app"; -import type { ConfigurationWmsVectorDetailsContent } from "../../../../@types/entrepot"; +import type { PyramidRaster, Service } from "../../../../@types/app"; +import type { BoundingBox, ConfigurationWmsVectorDetailsContent } from "../../../../@types/entrepot"; import DatastoreLayout from "../../../../components/Layout/DatastoreLayout"; +import LoadingIcon from "../../../../components/Utils/LoadingIcon"; import LoadingText from "../../../../components/Utils/LoadingText"; +import Wait from "../../../../components/Utils/Wait"; import ZoomRange from "../../../../components/Utils/ZoomRange"; import olDefaults from "../../../../data/ol-defaults.json"; import useScrollToTopEffect from "../../../../hooks/useScrollToTopEffect"; @@ -23,11 +28,23 @@ import { routes } from "../../../../router/router"; import api from "../../../api"; import { DatasheetViewActiveTabEnum } from "../../datasheet/DatasheetView/DatasheetView"; +function bboxToWkt(bbox: BoundingBox) { + const str = "POLYGON((west north,east north,east south,west south,west north))"; + + return str.replace(/[a-z]+/g, function (s) { + return bbox[s]; + }); +} + const STEPS = { TECHNICAL_NAME: 1, TOP_ZOOM_LEVEL: 2, }; +type PyramidRasterGenerateFormType = { + technical_name: string; +}; + type PyramidRasterGenerateFormProps = { datastoreId: string; offeringId: string; @@ -37,7 +54,7 @@ const PyramidRasterGenerateForm: FC = ({ datasto const { t } = useTranslation("PyramidRasterGenerateForm"); const { t: tCommon } = useTranslation("Common"); - const [currentStep, setCurrentStep] = useState(STEPS.TOP_ZOOM_LEVEL); + const [currentStep, setCurrentStep] = useState(STEPS.TECHNICAL_NAME); const serviceQuery = useQuery({ queryKey: RQKeys.datastore_offering(datastoreId, offeringId), @@ -45,32 +62,77 @@ const PyramidRasterGenerateForm: FC = ({ datasto staleTime: 60000, }); + const schemas = {}; + schemas[STEPS.TECHNICAL_NAME] = yup.object({ + technical_name: yup.string().typeError(t("technical_name.error.mandatory")).required(t("technical_name.error.mandatory")), + }); + + schemas[STEPS.TOP_ZOOM_LEVEL] = yup.lazy(() => { + // if (serviceQuery.data === undefined) { + // } + return yup.mixed().nullable().notRequired(); + }); + + const form = useForm({ + resolver: yupResolver(schemas[currentStep]), + mode: "onChange", + // values: defaultValues, + }); + + const { + register, + trigger, + getValues: getFormValues, + formState: { errors }, + } = form; + + // console.log(bboxToWkt((serviceQuery.data?.configuration.type_infos as ConfigurationWmsVectorDetailsContent).bbox)); + + const generatePyramidRasterMutation = useMutation({ + mutationFn: () => { + const formData = { + ...getFormValues(), + wmsv_offering_id: offeringId, + wmsv_config_bbox: bboxToWkt((serviceQuery.data?.configuration.type_infos as ConfigurationWmsVectorDetailsContent).bbox!), + }; + + console.log("formData", formData); + + return api.pyramidRaster.add(datastoreId, formData); + }, + onSuccess() { + // if (pyramidQuery.data?.tags?.datasheet_name) { + // queryClient.invalidateQueries({ + // queryKey: RQKeys.datastore_datasheet(datastoreId, pyramidQuery.data?.tags.datasheet_name), + // }); + // routes.datastore_datasheet_view({ datastoreId, datasheetName: pyramidQuery.data?.tags.datasheet_name, activeTab: "services" }).push(); + // } else { + // routes.datasheet_list({ datastoreId }).push(); + // } + }, + }); + useScrollToTopEffect(currentStep); const previousStep = useCallback(() => setCurrentStep((currentStep) => currentStep - 1), []); const nextStep = useCallback(async () => { - // const isStepValid = await trigger(undefined, { shouldFocus: true }); // demande de valider le formulaire - // if (!isStepValid) return; // ne fait rien si formulaire invalide + const isStepValid = await trigger(undefined, { shouldFocus: true }); // demande de valider le formulaire + if (!isStepValid) return; // ne fait rien si formulaire invalide // formulaire est valide if (currentStep < Object.values(STEPS).length) { // on passe à la prochaine étape du formulaire setCurrentStep((currentStep) => currentStep + 1); } else { // on est à la dernière étape du formulaire donc on envoie la sauce - // if (editMode) { - // editServiceMutation.mutate(); - // } else { - // createServiceMutation.mutate(); - // } + generatePyramidRasterMutation.mutate(); } - }, [ - currentStep, - /*createServiceMutation, editServiceMutation, trigger, editMode*/ - ]); + }, [currentStep, generatePyramidRasterMutation, trigger]); const [levels, setLevels] = useState([5, 15]); + console.log("errors", errors); + return (

{t("title")}

@@ -104,23 +166,26 @@ const PyramidRasterGenerateForm: FC = ({ datasto

{tCommon("mandatory_fields")}

+ {generatePyramidRasterMutation.error && ( + + )} +
-

Choisissez le nom technique de la pyramide de tuiles raster

+

{t("technical_name.lead_text")}

-

{"Choisissez le niveau de zoom top de vos tables"}

-

- {`Les niveaux de zoom de la pyramide de tuiles raster sont prédéfinis. Choisissez la borne minimum de votre pyramide de tuiles en vous aidant - de la carte de gauche. Le zoom maximum sur l’image de droite est fixe et ne peut être modifié. Tous les niveaux intermédiaires seront - générés.`} -

+

{t("top_zoom_level.lead_text")}

+

{t("top_zoom_level.explanation")}

{currentStep === STEPS.TOP_ZOOM_LEVEL && (serviceQuery.data?.configuration.type_infos as ConfigurationWmsVectorDetailsContent).used_data?.[0]?.relations.map((rel) => ( @@ -135,8 +200,9 @@ const PyramidRasterGenerateForm: FC = ({ datasto }} step={1} mode="top" + overlayContent={t("top_zoom_level.overlay_text")} /> - = ({ datasto setLevels(values); }} step={1} - /> + /> */} ))}
@@ -181,6 +247,21 @@ const PyramidRasterGenerateForm: FC = ({ datasto /> )} + + {generatePyramidRasterMutation.isPending && ( + +
+
+
+ +
+
+
{t("generate.in_progress")}
+
+
+
+
+ )}
); }; @@ -188,7 +269,19 @@ const PyramidRasterGenerateForm: FC = ({ datasto export default PyramidRasterGenerateForm; export const { i18n } = declareComponentKeys< - "title" | { K: "step.title"; P: { stepNumber: number }; R: string } | "wmsv-service.loading" | "wmsv-service.fetch_failed" | "back_to_datasheet" + | "title" + | { K: "step.title"; P: { stepNumber: number }; R: string } + | "wmsv-service.loading" + | "wmsv-service.fetch_failed" + | "back_to_datasheet" + | "technical_name.lead_text" + | "technical_name.label" + | "technical_name.explanation" + | "technical_name.error.mandatory" + | "top_zoom_level.lead_text" + | "top_zoom_level.explanation" + | "top_zoom_level.overlay_text" + | "generate.in_progress" >()({ PyramidRasterGenerateForm, }); @@ -209,6 +302,17 @@ export const PyramidRasterGenerateFormFrTranslations: Translations<"fr">["Pyrami "wmsv-service.loading": "Chargement du service WMS-Vecteur...", "wmsv-service.fetch_failed": "Récupération des informations sur le service WMS-Vecteur a échoué", back_to_datasheet: "Retour à la fiche de données", + "technical_name.lead_text": "Choisissez le nom technique de la pyramide de tuiles raster", + "technical_name.label": "Nom technique de la pyramide de tuiles raster", + "technical_name.explanation": + "II s'agit du nom technique du service qui apparaitra dans votre espace de travail, il ne sera pas publié en ligne. Si vous le renommez, choisissez un nom explicite.", + "technical_name.error.mandatory": "Le nom technique de la pyramide de tuiles raster est obligatoire", + "top_zoom_level.lead_text": "Choisissez le niveau de zoom top de vos tables", + "top_zoom_level.explanation": `Les niveaux de zoom de la pyramide de tuiles raster sont prédéfinis. Choisissez la borne minimum de votre pyramide de tuiles en vous aidant + de la carte de gauche. Le zoom maximum sur l’image de droite est fixe et ne peut être modifié. Tous les niveaux intermédiaires seront + générés.`, + "top_zoom_level.overlay_text": "Le zoom maximum est déterminé par la résolution des images fournies en entrée", + "generate.in_progress": "Génération de pyramide de tuiles raster en cours", }; export const PyramidRasterGenerateFormEnTranslations: Translations<"en">["PyramidRasterGenerateForm"] = { @@ -217,4 +321,12 @@ export const PyramidRasterGenerateFormEnTranslations: Translations<"en">["Pyrami "wmsv-service.loading": undefined, "wmsv-service.fetch_failed": undefined, back_to_datasheet: undefined, + "technical_name.error.mandatory": undefined, + "technical_name.lead_text": undefined, + "technical_name.label": undefined, + "technical_name.explanation": undefined, + "top_zoom_level.lead_text": undefined, + "top_zoom_level.explanation": undefined, + "top_zoom_level.overlay_text": undefined, + "generate.in_progress": undefined, }; diff --git a/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ProcessingExecutionReport.tsx b/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ProcessingExecutionReport.tsx index ff4c7995..e37e0cf7 100644 --- a/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ProcessingExecutionReport.tsx +++ b/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ProcessingExecutionReport.tsx @@ -24,6 +24,24 @@ const ProcessingExecutionReport: FC = ({ process
  • {"Identifiant technique de la donnée en sortie :"} {processingExecution?.output?.stored_data._id}{" "}
  • +
  • + Entrée : +
    +                    {JSON.stringify(processingExecution.inputs, null, 4)}
    +                
    +
  • +
  • + Sortie : +
    +                    {JSON.stringify(processingExecution.output, null, 4)}
    +                
    +
  • +
  • + Paramètres : +
    +                    {JSON.stringify(processingExecution.parameters, null, 4)}
    +                
    +
  • diff --git a/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ReportTab.tsx b/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ReportTab.tsx index 11f2f809..a0256622 100644 --- a/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ReportTab.tsx +++ b/assets/entrepot/pages/stored_data/StoredDataDetails/ReportTab/ReportTab.tsx @@ -78,6 +78,7 @@ const ReportTab: FC = ({ datastoreName, reportQuery }) => { defaultExpanded={[ ProcessingExecutionDetailResponseDtoStatusEnum.FAILURE, ProcessingExecutionDetailResponseDtoStatusEnum.ABORTED, + ProcessingExecutionDetailResponseDtoStatusEnum.PROGRESS, ].includes(procExec.status)} > diff --git a/assets/entrepot/pages/stored_data/StoredDataDetails/StoredDataDetails.tsx b/assets/entrepot/pages/stored_data/StoredDataDetails/StoredDataDetails.tsx index 66557a79..d5dbf2f3 100644 --- a/assets/entrepot/pages/stored_data/StoredDataDetails/StoredDataDetails.tsx +++ b/assets/entrepot/pages/stored_data/StoredDataDetails/StoredDataDetails.tsx @@ -3,9 +3,9 @@ import Alert from "@codegouvfr/react-dsfr/Alert"; import Button from "@codegouvfr/react-dsfr/Button"; import Tabs from "@codegouvfr/react-dsfr/Tabs"; import { useQuery } from "@tanstack/react-query"; -import { FC, useMemo } from "react"; +import { FC, useEffect, useMemo, useState } from "react"; -import { Datastore } from "../../../../@types/app"; +import { Datastore, StoredDataStatusEnum } from "../../../../@types/app"; import DatastoreLayout from "../../../../components/Layout/DatastoreLayout"; import LoadingIcon from "../../../../components/Utils/LoadingIcon"; import RQKeys from "../../../../modules/entrepot/RQKeys"; @@ -20,6 +20,8 @@ type StoredDataDetailsProps = { storedDataId: string; }; const StoredDataDetails: FC = ({ datastoreId, storedDataId }) => { + const [reportQueryEnabled, setReportQueryEnabled] = useState(true); + const datastoreQuery = useQuery({ queryKey: RQKeys.datastore(datastoreId), queryFn: ({ signal }) => api.datastore.get(datastoreId, { signal }), @@ -29,9 +31,19 @@ const StoredDataDetails: FC = ({ datastoreId, storedData const reportQuery = useQuery({ queryKey: RQKeys.datastore_stored_data_report(datastoreId, storedDataId), queryFn: ({ signal }) => api.storedData.getReportData(datastoreId, storedDataId, { signal }), - staleTime: 3600000, + refetchInterval: 60000, + enabled: reportQueryEnabled, }); + useEffect(() => { + if ( + reportQuery.data?.stored_data.status !== undefined && + [StoredDataStatusEnum.DELETED, StoredDataStatusEnum.GENERATED, StoredDataStatusEnum.UNSTABLE].includes(reportQuery.data?.stored_data.status) + ) { + setReportQueryEnabled(false); + } + }, [reportQuery.data?.stored_data.status]); + const datasheetName = useMemo(() => reportQuery?.data?.stored_data?.tags?.datasheet_name, [reportQuery?.data?.stored_data?.tags?.datasheet_name]); return ( @@ -81,6 +93,7 @@ const StoredDataDetails: FC = ({ datastoreId, storedData { label: "Rapport de génération", content: , + isDefault: true, }, ]} /> diff --git a/assets/i18n/i18n.ts b/assets/i18n/i18n.ts index 71264d4f..94a6cc9c 100644 --- a/assets/i18n/i18n.ts +++ b/assets/i18n/i18n.ts @@ -38,6 +38,7 @@ export type ComponentKey = | typeof import("../entrepot/pages/datasheet/DatasheetList/DatasheetList").i18n | typeof import("../entrepot/pages/datasheet/DatasheetView/DatasetListTab/VectorDbList/VectorDbListItem").i18n | typeof import("../entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidVectorList/PyramidVectorListItem").i18n + | typeof import("../entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterListItem").i18n | typeof import("../entrepot/pages/datasheet/DatasheetView/DatasheetView").i18n | typeof import("../config/navItems").i18n | typeof import("../config/datastoreNavItems").i18n diff --git a/assets/i18n/languages/en.tsx b/assets/i18n/languages/en.tsx index 9567ca4c..eb2e0d0d 100644 --- a/assets/i18n/languages/en.tsx +++ b/assets/i18n/languages/en.tsx @@ -6,7 +6,8 @@ import { CommunityMembersEnTranslations } from "../../entrepot/pages/communities import { contactEnTranslations } from "../../pages/assistance/contact/Contact"; import { DashboardProEnTranslations } from "../../entrepot/pages/dashboard/DashboardPro"; import { DatasheetListEnTranslations } from "../../entrepot/pages/datasheet/DatasheetList/DatasheetList"; -import { PyramidVectorListItemFrTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidVectorList/PyramidVectorListItem"; +import { PyramidVectorListItemEnTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidVectorList/PyramidVectorListItem"; +import { PyramidRasterListItemEnTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterListItem"; import { VectorDbListItemEnTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/VectorDbList/VectorDbListItem"; import { DatasheetViewEnTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasheetView"; import { DatastorePermissionsEnTranslations } from "../../entrepot/pages/datastore/ManagePermissions/DatastorePermissionsTr"; @@ -56,7 +57,8 @@ export const translations: Translations<"en"> = { navItems: navItemsEnTranslations, datastoreNavItems: datastoreNavItemsEnTranslations, VectorDbListItem: VectorDbListItemEnTranslations, - PyramidVectorListItem: PyramidVectorListItemFrTranslations, + PyramidVectorListItem: PyramidVectorListItemEnTranslations, + PyramidRasterListItem: PyramidRasterListItemEnTranslations, DatasheetView: DatasheetViewEnTranslations, SldStyleValidationErrors: SldStyleValidationErrorsEnTranslations, mapboxStyleValidation: mapboxStyleValidationEnTranslations, diff --git a/assets/i18n/languages/fr.tsx b/assets/i18n/languages/fr.tsx index 9dcf83d0..af1510cd 100644 --- a/assets/i18n/languages/fr.tsx +++ b/assets/i18n/languages/fr.tsx @@ -7,6 +7,7 @@ import { DashboardProFrTranslations } from "../../entrepot/pages/dashboard/Dashb import { DatasheetListFrTranslations } from "../../entrepot/pages/datasheet/DatasheetList/DatasheetList"; import { DatasheetUploadFormFrTranslations } from "../../entrepot/pages/datasheet/DatasheetNew/DatasheetUploadForm"; import { PyramidVectorListItemFrTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidVectorList/PyramidVectorListItem"; +import { PyramidRasterListItemFrTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidRasterList/PyramidRasterListItem"; import { VectorDbListItemFrTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/VectorDbList/VectorDbListItem"; import { DatasheetViewFrTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasheetView"; import { DatastorePermissionsFrTranslations } from "../../entrepot/pages/datastore/ManagePermissions/DatastorePermissionsTr"; @@ -57,6 +58,7 @@ export const translations: Translations<"fr"> = { datastoreNavItems: datastoreNavItemsFrTranslations, VectorDbListItem: VectorDbListItemFrTranslations, PyramidVectorListItem: PyramidVectorListItemFrTranslations, + PyramidRasterListItem: PyramidRasterListItemFrTranslations, DatasheetView: DatasheetViewFrTranslations, SldStyleValidationErrors: SldStyleValidationErrorsFrTranslations, mapboxStyleValidation: mapboxStyleValidationFrTranslations, diff --git a/assets/main.tsx b/assets/main.tsx index 1c40469d..3fee78ab 100644 --- a/assets/main.tsx +++ b/assets/main.tsx @@ -2,12 +2,12 @@ import { startReactDsfr } from "@codegouvfr/react-dsfr/spa"; import { disableReactDevTools } from "@fvilers/disable-react-devtools"; import React from "react"; import ReactDOM from "react-dom/client"; -import { mountStoreDevtool } from "simple-zustand-devtools"; +// import { mountStoreDevtool } from "simple-zustand-devtools"; import App from "./App"; -import { useApiEspaceCoStore } from "./stores/ApiEspaceCoStore"; -import { useAuthStore } from "./stores/AuthStore"; -import { useSnackbarStore } from "./stores/SnackbarStore"; +// import { useApiEspaceCoStore } from "./stores/ApiEspaceCoStore"; +// import { useAuthStore } from "./stores/AuthStore"; +// import { useSnackbarStore } from "./stores/SnackbarStore"; import "ol/ol.css"; @@ -17,9 +17,9 @@ if ((document.getElementById("root") as HTMLDivElement)?.dataset?.appEnv?.toLowe } // en dev/qualif else { - mountStoreDevtool("AuthStore", useAuthStore); - mountStoreDevtool("ApiEspaceCoStore", useApiEspaceCoStore); - mountStoreDevtool("SnackbarStore", useSnackbarStore); + // mountStoreDevtool("AuthStore", useAuthStore); + // mountStoreDevtool("ApiEspaceCoStore", useApiEspaceCoStore); + // mountStoreDevtool("SnackbarStore", useSnackbarStore); } startReactDsfr({ defaultColorScheme: "light" }); diff --git a/processing-pyramid-raster.json b/processing-pyramid-raster.json new file mode 100644 index 00000000..c394fd0f --- /dev/null +++ b/processing-pyramid-raster.json @@ -0,0 +1,182 @@ +{ + "name": "Calcul ou mise à jour de pyramide raster par moissonnage WMS", + "description": "Il n'y a pas besoin de donnée en entrée. Sont fournis en paramètres toutes les informations sur le service WMS et le jeu de données à moissonner, ainsi que la zone sur laquelle faire le moissonnage", + "input_types": { + "upload": [], + "stored_data": ["ROK4-PYRAMID-RASTER"] + }, + "output_type": { + "stored_data": "ROK4-PYRAMID-RASTER", + "storage": ["S3"] + }, + "parameters": [ + { + "name": "harvest_layers", + "description": "Couches à moisonner (séparées par des virgules)", + "mandatory": true, + "constraints": { + "type": "string" + } + }, + { + "name": "top", + "description": "Le niveau du haut de la pyramide en sortie ", + "mandatory": false, + "constraints": { + "type": "string" + } + }, + { + "name": "harvest_dimensions", + "description": "Deux entiers positifs, dimensions pixel maximales de moisonnage, devra être un diviseur de la taille pixel des dalles", + "mandatory": false, + "constraints": { + "type": "array", + "items": { + "type": "integer" + }, + "maxItems": 2, + "minItems": 2 + } + }, + { + "name": "compression", + "description": "La compression des données en sortie (valeurs possibles: raw, jpg, png, zip, jpg90)", + "mandatory": false, + "constraints": { + "enum": ["raw", "jpg", "png", "zip", "jpg90"], + "type": "string" + } + }, + { + "name": "samplesperpixel", + "description": "Nombre de canaux dans les dalles en sortie (entier de 1 à 4)", + "mandatory": false, + "constraints": { + "type": "integer", + "maximum": 4, + "minimum": 1 + } + }, + { + "name": "parallelization", + "description": "Le niveau de parallélisation du calcul (défaut à 1, entier >= 1)", + "mandatory": false, + "default_value": 1 + }, + { + "name": "tms", + "description": "L identifiant du quadrillage à utiliser (Tile Matrix Set)", + "mandatory": false, + "constraints": { + "enum": ["PM"], + "type": "string" + } + }, + { + "name": "height", + "description": "Le nombre de tuile par dalle en hauteur (entier >= 1)", + "mandatory": false, + "default_value": 16 + }, + { + "name": "harvest_levels", + "description": "Identifiants des niveaux pour lesquels on moissonne les dalles (celui le plus bas sera le niveau du bas de la pyramide). On considère que les niveaux sont précisés de bas en haut.", + "mandatory": true, + "constraints": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "sampleformat", + "description": "Format des canaux dans les dalles en sortie (UINT8 ou FLOAT32)", + "mandatory": false, + "constraints": { + "enum": ["UINT8", "FLOAT32"], + "type": "string" + } + }, + { + "name": "harvest_threshold", + "description": "Taille minimale en octet des dalles moissonnées", + "mandatory": false, + "constraints": { + "type": "integer", + "minimum": 0 + } + }, + { + "name": "harvest_extras", + "description": "Paramètres de requêtes GetMap additionnels, hors layers, bbox, format et srs", + "mandatory": false, + "constraints": { + "type": "string" + } + }, + { + "name": "width", + "description": "Le nombre de tuile par dalle en largeur (entier >= 1)", + "mandatory": false, + "default_value": 16 + }, + { + "name": "harvest_area", + "description": "WKT de la zone sur laquelle le moissonnage doit se faire, en EPSG:4326", + "mandatory": true, + "constraints": { + "type": "string" + } + }, + { + "name": "harvest_format", + "description": "Format des images téléchargées", + "mandatory": true, + "constraints": { + "enum": [ + "image/png", + "image/tiff", + "image/jpeg", + "image/x-bil;bits=32", + "image/tiff&format_options=compression:deflate", + "image/tiff&format_options=compression:lzw", + "image/tiff&format_options=compression:packbits", + "image/tiff&format_options=compression:raw" + ], + "type": "string" + } + }, + { + "name": "harvest_url", + "description": "URL du service WMS, avec le protocole et le chemin", + "mandatory": true, + "constraints": { + "type": "string" + } + }, + { + "name": "bottom", + "description": "Le niveau du bas de la pyramide en sortie ", + "mandatory": true, + "constraints": { + "type": "string" + } + }, + { + "name": "nodata", + "description": "Valeur de nodata pour compléter les images", + "mandatory": false, + "constraints": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1 + } + } + ], + "_id": "748e4ebe-3ef6-447d-8221-ecd3bc2157e6", + "required_checks": [] +} diff --git a/src/Controller/Entrepot/DatasheetController.php b/src/Controller/Entrepot/DatasheetController.php index cabc1336..b4a54c9d 100644 --- a/src/Controller/Entrepot/DatasheetController.php +++ b/src/Controller/Entrepot/DatasheetController.php @@ -103,20 +103,34 @@ public function getDetailed(string $datastoreId, string $datasheetName): JsonRes ]); // Pyramid vector - $pyramidList = $this->storedDataApiService->getAllDetailed($datastoreId, [ + $pyramidVectorList = $this->storedDataApiService->getAllDetailed($datastoreId, [ 'type' => StoredDataTypes::ROK4_PYRAMID_VECTOR, 'tags' => [ CommonTags::DATASHEET_NAME => $datasheetName, ], ]); + // Pyramid raster + $pyramidRasterList = $this->storedDataApiService->getAllDetailed($datastoreId, [ + 'type' => StoredDataTypes::ROK4_PYRAMID_RASTER, + 'tags' => [ + CommonTags::DATASHEET_NAME => $datasheetName, + ], + ]); + $metadataList = $this->metadataApiService->getAll($datastoreId, [ 'tags' => [ CommonTags::DATASHEET_NAME => $datasheetName, ], ]); - if (0 === count($uploadList) && 0 === count($vectorDbList) && 0 === count($pyramidList) && 0 === count($metadataList)) { + if ( + 0 === count($uploadList) + && 0 === count($vectorDbList) + && 0 === count($pyramidVectorList) + && 0 === count($pyramidRasterList) + && 0 === count($metadataList) + ) { throw new CartesApiException("La fiche de donnée [$datasheetName] n'existe pas", Response::HTTP_NOT_FOUND); } @@ -124,13 +138,14 @@ public function getDetailed(string $datastoreId, string $datasheetName): JsonRes $datasheet = $this->getBasicInfo($datastore, $datasheetName); // Recherche de services (configuration et offering) - $storedDataList = array_merge($vectorDbList, $pyramidList); + $storedDataList = array_merge([], $vectorDbList, $pyramidVectorList, $pyramidRasterList); $services = $this->_getServices($datastoreId, $storedDataList); return $this->json([ ...$datasheet, 'vector_db_list' => $vectorDbList, - 'pyramid_vector_list' => $pyramidList, + 'pyramid_vector_list' => $pyramidVectorList, + 'pyramid_raster_list' => $pyramidRasterList, 'upload_list' => $uploadList, 'service_list' => $services, ]); diff --git a/src/Controller/Entrepot/PyramidRasterController.php b/src/Controller/Entrepot/PyramidRasterController.php index 41341a43..37d5cece 100644 --- a/src/Controller/Entrepot/PyramidRasterController.php +++ b/src/Controller/Entrepot/PyramidRasterController.php @@ -2,7 +2,23 @@ namespace App\Controller\Entrepot; +use App\Constants\EntrepotApi\CommonTags; +use App\Constants\EntrepotApi\StoredDataTypes; use App\Controller\ApiControllerInterface; +use App\Exception\ApiException; +use App\Exception\AppException; +use App\Exception\CartesApiException; +use App\Services\CapabilitiesService; +use App\Services\EntrepotApi\CartesMetadataApiService; +use App\Services\EntrepotApi\CartesServiceApiService; +use App\Services\EntrepotApi\ConfigurationApiService; +use App\Services\EntrepotApi\DatastoreApiService; +use App\Services\EntrepotApi\ProcessingApiService; +use App\Services\EntrepotApi\StoredDataApiService; +use App\Services\SandboxService; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; #[Route( @@ -13,4 +29,97 @@ )] class PyramidRasterController extends ServiceController implements ApiControllerInterface { + public function __construct( + private DatastoreApiService $datastoreApiService, + private ConfigurationApiService $configurationApiService, + private StoredDataApiService $storedDataApiService, + private ProcessingApiService $processingApiService, + SandboxService $sandboxService, + CartesServiceApiService $cartesServiceApiService, + CapabilitiesService $capabilitiesService, + CartesMetadataApiService $cartesMetadataApiService, + ) { + parent::__construct($datastoreApiService, $configurationApiService, $cartesServiceApiService, $capabilitiesService, $cartesMetadataApiService, $sandboxService); + } + + /** + * @param array $bbox + */ + private function bboxToWkt(array $bbox): string + { + $str = 'POLYGON((west north,east north,east south,west south,west north))'; + + return preg_replace_callback('/[a-z]+/', function ($matches) use ($bbox) { + $key = $matches[0]; + + return $bbox[$key]; + }, $str); + } + + #[Route('/add', name: 'add', methods: ['POST'])] + public function add(string $datastoreId, Request $request): JsonResponse + { + try { + $data = json_decode($request->getContent(), true); + + $processingId = $this->sandboxService->getProcGeneratePyramidRaster($datastoreId); + + $wmsvOffering = $this->configurationApiService->getOffering($datastoreId, $data['wmsv_offering_id']); + $wmsvConfiguration = $this->configurationApiService->get($datastoreId, $wmsvOffering['configuration']['_id']); + + $vectorDbId = $wmsvConfiguration['type_infos']['used_data'][0]['stored_data'] ?? null; + if (null === $vectorDbId) { + throw new AppException(sprintf('Donnée stockée du type %s référencée par le service WMS-Vecteur non trouvée', StoredDataTypes::VECTOR_DB), Response::HTTP_BAD_REQUEST); + } + + $vectordb = $this->storedDataApiService->get($datastoreId, $vectorDbId); + + $serviceEndpoint = $this->datastoreApiService->getEndpoint($datastoreId, $wmsvOffering['endpoint']['_id']); + $harvestUrl = $serviceEndpoint['endpoint']['urls'][0]['url'] ?? null; + + if (null === $harvestUrl) { + throw new AppException('URL du service WMS-Vecteur non trouvée', Response::HTTP_BAD_REQUEST); + } + + $requestBody = [ + 'processing' => $processingId, + 'output' => [ + 'stored_data' => [ + 'name' => 'aaaaaaaaaaaa', // $data['technical_name'], + ], + ], + 'parameters' => [ + 'samplesperpixel' => 3, + 'sampleformat' => 'UINT8', + 'tms' => 'PM', + 'compression' => 'jpg', + 'bottom' => '14', + 'harvest_levels' => ['14', '10'], + 'harvest_format' => 'image/jpeg', + 'harvest_url' => $harvestUrl, + 'harvest_layers' => $wmsvOffering['layer_name'], + 'harvest_area' => $this->bboxToWkt($wmsvConfiguration['type_infos']['bbox']), + // 'POLYGON((1.999375 50.25875,5.8734375 50.25875,5.8734375 47.940898437,1.999375 47.940898437,1.999375 50.25875))', + ], + ]; + + $processingExecution = $this->processingApiService->addExecution($datastoreId, $requestBody); + $pyramidId = $processingExecution['output']['stored_data']['_id']; + + $pyramidTags = [ + CommonTags::DATASHEET_NAME => $vectordb['tags'][CommonTags::DATASHEET_NAME], + 'upload_id' => $vectordb['tags']['upload_id'], + 'proc_int_id' => $vectordb['tags']['proc_int_id'], + 'vectordb_id' => $vectorDbId, + 'proc_pyr_creat_id' => $processingExecution['_id'], + ]; + + $this->storedDataApiService->addTags($datastoreId, $pyramidId, $pyramidTags); + $this->processingApiService->launchExecution($datastoreId, $processingExecution['_id']); + + return new JsonResponse(); + } catch (ApiException|AppException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } } diff --git a/src/Controller/Entrepot/UserController.php b/src/Controller/Entrepot/UserController.php index a82cff7f..e3e301cd 100644 --- a/src/Controller/Entrepot/UserController.php +++ b/src/Controller/Entrepot/UserController.php @@ -34,11 +34,11 @@ public function getCurrentUser(): JsonResponse /** @var User */ $user = $this->getUser(); - $apiUserInfo = $this->userApiService->getMe(); + // $apiUserInfo = $this->userApiService->getMe(); - if (array_key_exists('communities_member', $apiUserInfo)) { - $user->setCommunitiesMember($apiUserInfo['communities_member']); - } + // if (array_key_exists('communities_member', $apiUserInfo)) { + // $user->setCommunitiesMember($apiUserInfo['communities_member']); + // } return $this->json($user); } diff --git a/src/Listener/SymfonyDebugToolbarSubscriber.php b/src/Listener/SymfonyDebugToolbarSubscriber.php new file mode 100644 index 00000000..735fac85 --- /dev/null +++ b/src/Listener/SymfonyDebugToolbarSubscriber.php @@ -0,0 +1,41 @@ + 'onKernelResponse', + ]; + } + + public function onKernelResponse(ResponseEvent $event): void + { + if (!$this->kernel->isDebug()) { + return; + } + + $request = $event->getRequest(); + if (!$request->isXmlHttpRequest()) { + return; + } + + $response = $event->getResponse(); + $response->headers->set('Symfony-Debug-Toolbar-Replace', '1'); + } +}