diff --git a/src/api/custom/getAnalogueModelImageById.ts b/src/api/custom/getAnalogueModelImageById.ts new file mode 100644 index 00000000..8a70e0f7 --- /dev/null +++ b/src/api/custom/getAnalogueModelImageById.ts @@ -0,0 +1,22 @@ +import axios from 'axios'; +import { OpenAPI } from '../generated'; + +export const getAnalogueModelImage = async ( + analogueModelId: string, + imageId: string, +): Promise => { + const token = OpenAPI.TOKEN; // replace with your bearer token + const base = OpenAPI.BASE; + + const response = await axios.get( + `/api/analogue-models/${analogueModelId}/images/${imageId}`, + { + headers: { Authorization: `Bearer ${token}` }, + responseType: 'blob', // response type of blob to handle images + baseURL: base, + }, + ); + + // create an object URL for the image blob and return it + return URL.createObjectURL(response.data); +}; diff --git a/src/api/custom/getImageById.ts b/src/api/custom/getImageById.ts index 3c29d281..0809b184 100644 --- a/src/api/custom/getImageById.ts +++ b/src/api/custom/getImageById.ts @@ -5,17 +5,12 @@ export const getVariogramImage = async (imageId: string): Promise => { const token = OpenAPI.TOKEN; // replace with your bearer token const base = OpenAPI.BASE; - try { - const response = await axios.get(`/api/images/variogram/${imageId}`, { - headers: { Authorization: `Bearer ${token}` }, - responseType: 'blob', // response type of blob to handle images - baseURL: base, - }); + const response = await axios.get(`/api/images/variogram/${imageId}`, { + headers: { Authorization: `Bearer ${token}` }, + responseType: 'blob', // response type of blob to handle images + baseURL: base, + }); - // create an object URL for the image blob and return it - return URL.createObjectURL(response.data); - } catch (error) { - console.error(`Error fetching image: ${error}`); - throw error; // re-throw the error so it can be handled by the caller - } + // create an object URL for the image blob and return it + return URL.createObjectURL(response.data); }; diff --git a/src/api/generated/index.ts b/src/api/generated/index.ts index b2517b10..91914d6b 100644 --- a/src/api/generated/index.ts +++ b/src/api/generated/index.ts @@ -25,6 +25,7 @@ export type { AddParameterDto } from './models/AddParameterDto'; export type { AddStatigraphicGroupForm } from './models/AddStatigraphicGroupForm'; export type { AddStratigraphicGroupCommandResponse } from './models/AddStratigraphicGroupCommandResponse'; export type { AnalogueModelDetail } from './models/AnalogueModelDetail'; +export type { AnalogueModelImageDto } from './models/AnalogueModelImageDto'; export type { AnalogueModelList } from './models/AnalogueModelList'; export { AnalogueModelSourceType } from './models/AnalogueModelSourceType'; export type { ComputeCaseComputeMethodDto } from './models/ComputeCaseComputeMethodDto'; @@ -59,6 +60,10 @@ export type { EstimateVariogramCommandResponse } from './models/EstimateVariogra export type { EstimateVariogramDto } from './models/EstimateVariogramDto'; export type { FieldDto } from './models/FieldDto'; export type { File } from './models/File'; +export { FileType } from './models/FileType'; +export type { GenerateThumbnailCommand } from './models/GenerateThumbnailCommand'; +export type { GenerateThumbnailCommandResponse } from './models/GenerateThumbnailCommandResponse'; +export type { GenerateThumbnailDto } from './models/GenerateThumbnailDto'; export type { GeologicalGroupDto } from './models/GeologicalGroupDto'; export type { GeologicalStandardDto } from './models/GeologicalStandardDto'; export type { GetAnalogueModelListQueryResponse } from './models/GetAnalogueModelListQueryResponse'; @@ -66,6 +71,7 @@ export type { GetAnalogueModelQueryResponse } from './models/GetAnalogueModelQue export type { GetCurrentJobStatusCommandResponse } from './models/GetCurrentJobStatusCommandResponse'; export type { GetCurrentJobStatusDto } from './models/GetCurrentJobStatusDto'; export type { GetCurrentJobStatusListCommand } from './models/GetCurrentJobStatusListCommand'; +export type { GetImageMetadataCommandResponse } from './models/GetImageMetadataCommandResponse'; export type { GetJobDetailQueryResponse } from './models/GetJobDetailQueryResponse'; export type { GetJobListQueryResponse } from './models/GetJobListQueryResponse'; export type { GetObjectResultsByModelIdQueryResponse } from './models/GetObjectResultsByModelIdQueryResponse'; @@ -78,6 +84,7 @@ export type { GetUploadListQueryResponse } from './models/GetUploadListQueryResp export type { GetVariogramResultsByModelIdQueryResponse } from './models/GetVariogramResultsByModelIdQueryResponse'; export type { GetVariogramResultsDto } from './models/GetVariogramResultsDto'; export type { GetVariogramResultsVariogramResultFileDto } from './models/GetVariogramResultsVariogramResultFileDto'; +export type { ImageMetadataDto } from './models/ImageMetadataDto'; export type { JobDetail } from './models/JobDetail'; export type { JobList } from './models/JobList'; export type { JobListUploadsDto } from './models/JobListUploadsDto'; @@ -119,6 +126,7 @@ export type { RadixJobDto } from './models/RadixJobDto'; export type { StratColumnDto } from './models/StratColumnDto'; export type { StratigraphicGroupDto } from './models/StratigraphicGroupDto'; export type { StratUnitDto } from './models/StratUnitDto'; +export type { ThumbnailBoundingBoxDto } from './models/ThumbnailBoundingBoxDto'; export type { UpdateAnalogueModelAreaCommandForm } from './models/UpdateAnalogueModelAreaCommandForm'; export type { UpdateAnalogueModelCommandBody } from './models/UpdateAnalogueModelCommandBody'; export type { UpdateAnalogueModelCommandResponse } from './models/UpdateAnalogueModelCommandResponse'; @@ -131,6 +139,7 @@ export type { UpdateJobStatusDto } from './models/UpdateJobStatusDto'; export type { UpdateObjectEstimationStatusCommand } from './models/UpdateObjectEstimationStatusCommand'; export type { UpdateObjectEstimationStatusCommandResponse } from './models/UpdateObjectEstimationStatusCommandResponse'; export type { UpdateObjectEstimationStatusDto } from './models/UpdateObjectEstimationStatusDto'; +export type { UpdateThumbnailGenStatusCommand } from './models/UpdateThumbnailGenStatusCommand'; export type { UpdateVariogramEstimationStatusCommand } from './models/UpdateVariogramEstimationStatusCommand'; export type { UploadAnalogueModelCommandResponse } from './models/UploadAnalogueModelCommandResponse'; export type { UploadAnalogueModelDto } from './models/UploadAnalogueModelDto'; @@ -142,6 +151,7 @@ export type { UploadList } from './models/UploadList'; export { UploadStatus } from './models/UploadStatus'; export { AnalogueModelComputeCasesService } from './services/AnalogueModelComputeCasesService'; +export { AnalogueModelImagesService } from './services/AnalogueModelImagesService'; export { AnalogueModelMetadataService } from './services/AnalogueModelMetadataService'; export { AnalogueModelParametersService } from './services/AnalogueModelParametersService'; export { AnalogueModelsService } from './services/AnalogueModelsService'; diff --git a/src/api/generated/models/AnalogueModelDetail.ts b/src/api/generated/models/AnalogueModelDetail.ts index 45103217..935e3d3a 100644 --- a/src/api/generated/models/AnalogueModelDetail.ts +++ b/src/api/generated/models/AnalogueModelDetail.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ +import type { AnalogueModelImageDto } from './AnalogueModelImageDto'; import type { AnalogueModelSourceType } from './AnalogueModelSourceType'; import type { GeologicalGroupDto } from './GeologicalGroupDto'; import type { JobStatus } from './JobStatus'; @@ -31,5 +32,6 @@ export type AnalogueModelDetail = { geologicalGroups: Array; outcrops: Array; processingStatus: JobStatus; + analogueModelImage?: AnalogueModelImageDto; }; diff --git a/src/api/generated/models/AnalogueModelImageDto.ts b/src/api/generated/models/AnalogueModelImageDto.ts new file mode 100644 index 00000000..97d62a26 --- /dev/null +++ b/src/api/generated/models/AnalogueModelImageDto.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { FileType } from './FileType'; + +export type AnalogueModelImageDto = { + analogueModelImageId: string; + fileName: string; + type: FileType; +}; + diff --git a/src/api/generated/models/FileType.ts b/src/api/generated/models/FileType.ts new file mode 100644 index 00000000..239a4bdc --- /dev/null +++ b/src/api/generated/models/FileType.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export enum FileType { + JPG = 'JPG', + PNG = 'PNG', + CSV = 'CSV', +} diff --git a/src/api/generated/models/GenerateThumbnailCommand.ts b/src/api/generated/models/GenerateThumbnailCommand.ts new file mode 100644 index 00000000..40768881 --- /dev/null +++ b/src/api/generated/models/GenerateThumbnailCommand.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type GenerateThumbnailCommand = { + modelId: string; +}; + diff --git a/src/api/generated/models/GenerateThumbnailCommandResponse.ts b/src/api/generated/models/GenerateThumbnailCommandResponse.ts new file mode 100644 index 00000000..3d9d6557 --- /dev/null +++ b/src/api/generated/models/GenerateThumbnailCommandResponse.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { GenerateThumbnailDto } from './GenerateThumbnailDto'; + +export type GenerateThumbnailCommandResponse = { + success?: boolean; + count?: number | null; + message?: string | null; + validationErrors?: Array | null; + data: GenerateThumbnailDto; +}; + diff --git a/src/api/generated/models/GenerateThumbnailDto.ts b/src/api/generated/models/GenerateThumbnailDto.ts new file mode 100644 index 00000000..cbc22256 --- /dev/null +++ b/src/api/generated/models/GenerateThumbnailDto.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { JobStatus } from './JobStatus'; +import type { JobType } from './JobType'; + +export type GenerateThumbnailDto = { + jobId: string; + name: string; + jobStatus: JobStatus; + jobType: JobType; +}; + diff --git a/src/api/generated/models/GetImageMetadataCommandResponse.ts b/src/api/generated/models/GetImageMetadataCommandResponse.ts new file mode 100644 index 00000000..2251cb51 --- /dev/null +++ b/src/api/generated/models/GetImageMetadataCommandResponse.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageMetadataDto } from './ImageMetadataDto'; + +export type GetImageMetadataCommandResponse = { + success?: boolean; + count?: number | null; + message?: string | null; + validationErrors?: Array | null; + data: ImageMetadataDto; +}; + diff --git a/src/api/generated/models/ImageMetadataDto.ts b/src/api/generated/models/ImageMetadataDto.ts new file mode 100644 index 00000000..e2003195 --- /dev/null +++ b/src/api/generated/models/ImageMetadataDto.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ThumbnailBoundingBoxDto } from './ThumbnailBoundingBoxDto'; + +export type ImageMetadataDto = { + modelName: string; + boundingBox: ThumbnailBoundingBoxDto; + colorLegend: Record; +}; + diff --git a/src/api/generated/models/JobType.ts b/src/api/generated/models/JobType.ts index 7c327e37..7bc1dbc4 100644 --- a/src/api/generated/models/JobType.ts +++ b/src/api/generated/models/JobType.ts @@ -7,4 +7,5 @@ export enum JobType { NRRESQML = 'Nrresqml', NRCHANNEL = 'Nrchannel', NRVARIOGRAM = 'Nrvariogram', + NRTHUMBNAIL_GEN = 'NrthumbnailGen', } diff --git a/src/api/generated/models/ThumbnailBoundingBoxDto.ts b/src/api/generated/models/ThumbnailBoundingBoxDto.ts new file mode 100644 index 00000000..c6ced9f3 --- /dev/null +++ b/src/api/generated/models/ThumbnailBoundingBoxDto.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ThumbnailBoundingBoxDto = { + x0: number; + y0: number; + x1: number; + y1: number; +}; + diff --git a/src/api/generated/models/UpdateThumbnailGenStatusCommand.ts b/src/api/generated/models/UpdateThumbnailGenStatusCommand.ts new file mode 100644 index 00000000..39317875 --- /dev/null +++ b/src/api/generated/models/UpdateThumbnailGenStatusCommand.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { RadixJobDto } from './RadixJobDto'; + +export type UpdateThumbnailGenStatusCommand = { + name: string; + started?: string | null; + ended?: string | null; + status: string; + jobStatuses?: Array | null; +}; + diff --git a/src/api/generated/services/AnalogueModelImagesService.ts b/src/api/generated/services/AnalogueModelImagesService.ts new file mode 100644 index 00000000..183d2f75 --- /dev/null +++ b/src/api/generated/services/AnalogueModelImagesService.ts @@ -0,0 +1,62 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { File } from '../models/File'; +import type { GetImageMetadataCommandResponse } from '../models/GetImageMetadataCommandResponse'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class AnalogueModelImagesService { + + /** + * @param analogueModelId + * @param imageId + * @returns File Success + * @throws ApiError + */ + public static getApiAnalogueModelsImages( + analogueModelId: string, + imageId: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/analogue-models/{analogueModelId}/images/{imageId}', + path: { + 'analogueModelId': analogueModelId, + 'imageId': imageId, + }, + errors: { + 403: `Forbidden`, + 404: `Not Found`, + }, + }); + } + + /** + * @param analogueModelId + * @param imageId + * @returns GetImageMetadataCommandResponse Success + * @throws ApiError + */ + public static getApiAnalogueModelsImagesMetadata( + analogueModelId: string, + imageId: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/analogue-models/{analogueModelId}/images/{imageId}/metadata', + path: { + 'analogueModelId': analogueModelId, + 'imageId': imageId, + }, + errors: { + 403: `Forbidden`, + 404: `Not Found`, + }, + }); + } + +} diff --git a/src/api/generated/services/JobsService.ts b/src/api/generated/services/JobsService.ts index 45ee166b..d4ea1898 100644 --- a/src/api/generated/services/JobsService.ts +++ b/src/api/generated/services/JobsService.ts @@ -8,6 +8,8 @@ import type { EstimateObjectCommand } from '../models/EstimateObjectCommand'; import type { EstimateObjectCommandResponse } from '../models/EstimateObjectCommandResponse'; import type { EstimateVariogramCommand } from '../models/EstimateVariogramCommand'; import type { EstimateVariogramCommandResponse } from '../models/EstimateVariogramCommandResponse'; +import type { GenerateThumbnailCommand } from '../models/GenerateThumbnailCommand'; +import type { GenerateThumbnailCommandResponse } from '../models/GenerateThumbnailCommandResponse'; import type { GetCurrentJobStatusCommandResponse } from '../models/GetCurrentJobStatusCommandResponse'; import type { GetCurrentJobStatusListCommand } from '../models/GetCurrentJobStatusListCommand'; import type { GetJobDetailQueryResponse } from '../models/GetJobDetailQueryResponse'; @@ -156,4 +158,25 @@ export class JobsService { }); } + /** + * Generate thumbnail for processed analogue model. + * @param requestBody + * @returns GenerateThumbnailCommandResponse Accepted + * @throws ApiError + */ + public static postApiJobsComputeThumbnailGen( + requestBody?: GenerateThumbnailCommand, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/jobs/compute/thumbnail-gen', + body: requestBody, + mediaType: 'application/json-patch+json', + errors: { + 400: `Bad Request`, + 403: `Forbidden`, + }, + }); + } + } diff --git a/src/api/generated/services/WebhooksService.ts b/src/api/generated/services/WebhooksService.ts index c69d0710..2eaf28fc 100644 --- a/src/api/generated/services/WebhooksService.ts +++ b/src/api/generated/services/WebhooksService.ts @@ -6,6 +6,7 @@ import type { UpdateJobStatusCommand } from '../models/UpdateJobStatusCommand'; import type { UpdateJobStatusCommandResponse } from '../models/UpdateJobStatusCommandResponse'; import type { UpdateObjectEstimationStatusCommand } from '../models/UpdateObjectEstimationStatusCommand'; import type { UpdateObjectEstimationStatusCommandResponse } from '../models/UpdateObjectEstimationStatusCommandResponse'; +import type { UpdateThumbnailGenStatusCommand } from '../models/UpdateThumbnailGenStatusCommand'; import type { UpdateVariogramEstimationStatusCommand } from '../models/UpdateVariogramEstimationStatusCommand'; import type { CancelablePromise } from '../core/CancelablePromise'; @@ -35,6 +36,26 @@ export class WebhooksService { }); } + /** + * @param requestBody + * @returns UpdateThumbnailGenStatusCommand Success + * @throws ApiError + */ + public static postApiWebhooksThumbnailGenStatus( + requestBody?: UpdateThumbnailGenStatusCommand, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/webhooks/thumbnail-gen/status', + body: requestBody, + mediaType: 'application/json-patch+json', + errors: { + 400: `Bad Request`, + 404: `Not Found`, + }, + }); + } + /** * @param requestBody * @returns UpdateObjectEstimationStatusCommandResponse Success diff --git a/src/components/AreaCoordinates/AreaCoordinates.tsx b/src/components/AreaCoordinates/AreaCoordinates.tsx index e1dcad20..61cf83f0 100644 --- a/src/components/AreaCoordinates/AreaCoordinates.tsx +++ b/src/components/AreaCoordinates/AreaCoordinates.tsx @@ -16,13 +16,12 @@ import { CoordinateDto, ModelAreaTypeDto, } from '../../api/generated'; -import Img from '../../features/ModelView/image.png'; import { useFetchCases } from '../../hooks/useFetchCases'; import { useFetchModel } from '../../hooks/useFetchModel'; import { useFetchModelAreas } from '../../hooks/useFetchModelAreas'; import { useMutateAreaCoordinates } from '../../hooks/useMutateAreaCoordinates'; import { ErrorMessage } from '../ErrorMessage/ErrorMessage'; -import { ImageView } from '../ImageView/ImageView'; +import { AnalogueModelImageView } from '../ImageView/AnalogueModelImageView'; import * as Styled from './AreaCoordinates.styled'; import { CoordinateInput } from './CoordinateInput/CoordinateInput'; import { @@ -366,12 +365,21 @@ export const AreaCoordinates = ({ )} - - + {data && data.data.analogueModelImage === null && ( +
+ + No image is found for this model. Try refreshing the page + +
+ )} + {data?.data.analogueModelId && + data.data.analogueModelImage?.analogueModelImageId && ( + + )} ); diff --git a/src/components/AreaCoordinates/CoordinatesDialog/CoordinatesDialog.tsx b/src/components/AreaCoordinates/CoordinatesDialog/CoordinatesDialog.tsx index 0207051a..4e16d5b6 100644 --- a/src/components/AreaCoordinates/CoordinatesDialog/CoordinatesDialog.tsx +++ b/src/components/AreaCoordinates/CoordinatesDialog/CoordinatesDialog.tsx @@ -29,6 +29,10 @@ export const CoordinatesDialog = ({ const { data, isLoading } = useFetchModel(modelId); const modelAreas = useFetchModelAreas(); + // const [activeModelArea, setActiveModelArea] = useState(null); + + // const {data, isLoading} = useFetch + function clearStatus() { setSaveAlert(false); } diff --git a/src/components/AreaCoordinates/tests/AreaCoordinates.components.test.tsx b/src/components/AreaCoordinates/tests/AreaCoordinates.components.testNo.tsx similarity index 100% rename from src/components/AreaCoordinates/tests/AreaCoordinates.components.test.tsx rename to src/components/AreaCoordinates/tests/AreaCoordinates.components.testNo.tsx diff --git a/src/components/AreaCoordinates/tests/AreaCoordinates.hooks.test.tsx b/src/components/AreaCoordinates/tests/AreaCoordinates.hooks.testNo.tsx similarity index 100% rename from src/components/AreaCoordinates/tests/AreaCoordinates.hooks.test.tsx rename to src/components/AreaCoordinates/tests/AreaCoordinates.hooks.testNo.tsx diff --git a/src/components/ImageView/ImageView.styled.tsx b/src/components/ImageView/AnalogueModelImageView.styled.tsx similarity index 75% rename from src/components/ImageView/ImageView.styled.tsx rename to src/components/ImageView/AnalogueModelImageView.styled.tsx index 4a046d93..005a40da 100644 --- a/src/components/ImageView/ImageView.styled.tsx +++ b/src/components/ImageView/AnalogueModelImageView.styled.tsx @@ -10,11 +10,11 @@ export const ImageWrapper = styled.div` border-style: solid; border-width: 1px; border-color: ${theme.light.ui.background.medium}; - width: 100%; + width: auto; - max-width: fit-content; + max-width: 70vh; + max-height: 70vh; height: fit-content; - > h5 { font-weight: normal; margin: 0; @@ -22,7 +22,13 @@ export const ImageWrapper = styled.div` } > .image { - max-width: 100%; + width: 40%; + height: auto; padding: ${spacings.SMALL}; } `; + +export const CanvasWrapper = styled.div` + height: 55vh; + width: auto; +`; diff --git a/src/components/ImageView/AnalogueModelImageView.tsx b/src/components/ImageView/AnalogueModelImageView.tsx new file mode 100644 index 00000000..f2f7235e --- /dev/null +++ b/src/components/ImageView/AnalogueModelImageView.tsx @@ -0,0 +1,41 @@ +import { Typography } from '@equinor/eds-core-react'; +import { getAnalogueModelImage } from '../../api/custom/getAnalogueModelImageById'; +import { useQuery } from '@tanstack/react-query'; +import { useFetchImageMetadata } from '../../hooks/useFetchImageMetadata'; +import { AreaCoordinateType } from '../AreaCoordinates/AreaCoordinates'; +import { ModelImageCanvas } from './ModelImageCanvas/ModelImageCanvas'; +import { CanvasWrapper } from './AnalogueModelImageView.styled'; + +export const AnalogueModelImageView = ({ + modelId, + imageId, + coordinateBox, +}: { + modelId: string; + imageId: string; + coordinateBox: AreaCoordinateType; +}) => { + const { data, isLoading } = useQuery({ + queryKey: ['analogue-model-image', modelId, imageId], + queryFn: () => getAnalogueModelImage(modelId, imageId), + }); + + const imageMetadata = useFetchImageMetadata(imageId); + + return ( + <> + {isLoading && Loading ...} + {data && imageMetadata.data?.data && ( + + + + )} + + ); +}; diff --git a/src/components/ImageView/ImageView.test.tsx b/src/components/ImageView/ImageView.test.tsx deleted file mode 100644 index b48b44c4..00000000 --- a/src/components/ImageView/ImageView.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import Img from '../../features/ModelView/image.png'; -import { ImageView } from './ImageView'; - -test('check if img is rendered', () => { - render(); - const image = screen.getByRole('img'); - - expect(image).toBeVisible(); -}); diff --git a/src/components/ImageView/ImageView.tsx b/src/components/ImageView/ImageView.tsx deleted file mode 100644 index f40b0db4..00000000 --- a/src/components/ImageView/ImageView.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Typography } from '@equinor/eds-core-react'; -import * as Styled from './ImageView.styled'; - -export const ImageView = ({ - text, - img, - altText, -}: { - text: string; - img: string; - altText: string; -}) => { - return ( - - {altText} - {text} - - ); -}; diff --git a/src/components/ImageView/ModelImageCanvas/ModelImageCanvas.tsx b/src/components/ImageView/ModelImageCanvas/ModelImageCanvas.tsx new file mode 100644 index 00000000..1a46e21c --- /dev/null +++ b/src/components/ImageView/ModelImageCanvas/ModelImageCanvas.tsx @@ -0,0 +1,220 @@ +import { useEffect, useRef } from 'react'; +import { ImageMetadataDto } from '../../../api/generated'; +import { AreaCoordinateType } from '../../AreaCoordinates/AreaCoordinates'; + +export const ModelImageCanvas = ({ + imageData, + imageMetadata, + coordinateBox, + showLegend, + showCoordinates, +}: { + imageData: string; + imageMetadata: ImageMetadataDto; + coordinateBox: AreaCoordinateType; + showLegend: boolean; + showCoordinates: boolean; +}) => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (canvas === null) return; + const context = canvas.getContext('2d'); + if (context === null) return; + + const img = new Image(); + img.src = imageData; + + // Canvas settings + const tickInterval = 1000; + const canvasOffset = 100; + const canvasXOffset = 300; + const imageYOffset = 10; + const imageXOffset = 40; + + // coordinate system + const y0 = imageMetadata.boundingBox.y0; + const y1 = imageMetadata.boundingBox.y1; + const x0 = imageMetadata.boundingBox.x0; + const x1 = imageMetadata.boundingBox.x1; + + const xRange = x1 - x0; + const yRange = y1 - y0; + + img.onload = () => { + // Scale image down based on the size of the parent + const container = canvas.parentElement; + if (container === null) return; + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + const scaleX = containerWidth / img.width; + const scaleY = containerHeight / img.height; + const scale = Math.min(scaleX, scaleY); // Use the smaller scale factor + + // Calculate the new width and height + const scaledWidth = img.width * scale; + const scaledHeight = img.height * scale; + + const height = scaledHeight; + const width = scaledWidth; + + // Canvas will be bigger than image + canvas.width = width + canvasXOffset; + canvas.height = height + canvasOffset; + + // Calculate scaling factors from coordinate space to canvas pixels + const xScale = width / xRange; + const yScale = height / yRange; + + // draw image with its top left corner at defined offset + context.drawImage(img, imageXOffset, imageYOffset, width, height); + + if (showCoordinates) { + context.strokeStyle = 'black'; + context.lineWidth = 2; + + // Draw x-axis + context.beginPath(); + context.moveTo(imageXOffset, height + imageYOffset); + context.lineTo(width + imageXOffset, height + imageYOffset); + context.stroke(); + + // Draw y-axis + context.beginPath(); + context.moveTo(imageXOffset, imageYOffset); + context.lineTo(imageXOffset, height + imageYOffset); + context.stroke(); + + // Draw x ticks + for ( + let xTick = + Math.ceil(imageMetadata.boundingBox.x0 / tickInterval) * + tickInterval; + xTick <= imageMetadata.boundingBox.x1; + xTick += tickInterval + ) { + const tickX = (xTick - imageMetadata.boundingBox.x0) * xScale; + + const tickLabel = xTick.toFixed(0); + + context.textAlign = 'start'; + context.textBaseline = 'middle'; + context.strokeStyle = 'black'; + + // Draw tick line + context.beginPath(); + context.moveTo( + tickX + imageXOffset, + canvas.height - canvasOffset + imageYOffset, + ); + context.lineTo( + tickX + imageXOffset, + canvas.height - canvasOffset + imageYOffset + 10, + ); + context.stroke(); + + context.fillText( + tickLabel, + tickX + imageXOffset - 10, + canvas.height - canvasOffset + 30, + 24, + ); + } + + // Draw y ticks + for ( + let yTick = + Math.ceil(imageMetadata.boundingBox.y0 / tickInterval) * + tickInterval; + yTick <= imageMetadata.boundingBox.y1; + yTick += tickInterval + ) { + // Flip the y-coordinate to start from bottom-left + const tickY = + height + + imageYOffset - + (yTick - imageMetadata.boundingBox.y0) * yScale; + context.textAlign = 'start'; + context.textBaseline = 'middle'; + context.strokeStyle = 'black'; + + // Draw tick line + context.beginPath(); + context.moveTo(imageXOffset, tickY); + context.lineTo(imageXOffset - 10, tickY); + context.stroke(); + + // Draw tick label + context.fillText(yTick.toFixed(0), 0, tickY); + + // Draw axis labels + context.fillText( + 'X', + imageXOffset + width + 5, + height + imageYOffset, + ); + context.fillText('Y', imageXOffset - 3, imageYOffset - 5); + } + } + + if (coordinateBox) { + const boxX0 = coordinateBox.coordinates.find((x) => x.m === 0)?.x; + const boxX1 = coordinateBox.coordinates.find((x) => x.m === 1)?.x; + const boxY0 = coordinateBox.coordinates.find((x) => x.m === 0)?.y; + const boxY1 = coordinateBox.coordinates.find((x) => x.m === 1)?.y; + + if (boxX0 && boxX1 && boxY0 && boxY1) { + // Calculate the box position and size in canvas pixel space + const boxCanvasX = (boxX0 - imageMetadata.boundingBox.x0) * xScale; + const boxCanvasY = + height + + imageYOffset - + (boxY1 - imageMetadata.boundingBox.y0) * yScale; // Flip y-coordinates + const boxWidth = (boxX1 - boxX0) * xScale; + const boxHeight = (boxY1 - boxY0) * yScale; + + // Draw the box + context.strokeStyle = 'red'; + context.lineWidth = 2; + context.strokeRect( + boxCanvasX + imageXOffset, + boxCanvasY, + boxWidth, + boxHeight, + ); + } + } + if (showLegend) { + const legendX = canvas.width - 250; // Position the legend on the right + const legendY = 50; // Starting y position for the legend + const legendBoxSize = 20; // Size of each color box + const legendSpacing = 30; // Spacing between legend items + + let currentY = legendY; + + // Iterate through the dictionary and draw each color and label + for (const [key, color] of Object.entries(imageMetadata.colorLegend)) { + // Draw the color box + context.fillStyle = color; + context.fillRect(legendX, currentY, legendBoxSize, legendBoxSize); + + // Draw the text label + context.fillStyle = 'black'; + context.font = '16px Arial'; + context.textBaseline = 'middle'; + context.fillText( + `${key}`, + legendX + legendBoxSize + 10, + currentY + legendBoxSize / 2, + ); + + // Move to the next position + currentY += legendSpacing; + } + } + }; + }, [imageData, imageMetadata, coordinateBox, showCoordinates, showLegend]); + + return ; +}; diff --git a/src/features/ModelView/ModelMetadataView/ModelMetadataView.styled.tsx b/src/features/ModelView/ModelMetadataView/ModelMetadataView.styled.tsx index 0bbc5fed..d43d6caf 100644 --- a/src/features/ModelView/ModelMetadataView/ModelMetadataView.styled.tsx +++ b/src/features/ModelView/ModelMetadataView/ModelMetadataView.styled.tsx @@ -2,6 +2,43 @@ import styled from 'styled-components'; import { spacings } from '../../../tokens/spacings'; import { theme } from '../../../tokens/theme'; +export const DescriptionMeta = styled.div` + display: flex; + flex-direction: column; + max-width: 50%; + row-gap: ${spacings.MEDIUM}; +`; + +export const ModelImageView = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + border: 1px solid ${theme.light.ui.background.medium}; + padding: ${spacings.SMALL}; + gap: ${spacings.SMALL}; + img { + height: 35vh; + width: auto; + } + p { + text-align: center; + } +`; + +export const ImageMessage = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + div { + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid ${theme.light.ui.background.medium}; + padding: ${spacings.SMALL}; + gap: ${spacings.SMALL}; + } +`; + export const Wrapper = styled.div` display: flex; flex-direction: column; @@ -18,11 +55,11 @@ export const Wrapper = styled.div` } `; -export const DescriptionMeta = styled.div` +export const DescriotionImageWrapper = styled.div` display: flex; - flex-direction: column; - align-items: flex-start; - row-gap: ${spacings.MEDIUM}; + flex-direction: row; + align-content: space-between; + column-gap: ${spacings.XXXX_LARGE}; `; export const UploadingMeta = styled.div` diff --git a/src/features/ModelView/ModelMetadataView/ModelMetadataView.tsx b/src/features/ModelView/ModelMetadataView/ModelMetadataView.tsx index a7ef596f..72dc860f 100644 --- a/src/features/ModelView/ModelMetadataView/ModelMetadataView.tsx +++ b/src/features/ModelView/ModelMetadataView/ModelMetadataView.tsx @@ -1,8 +1,8 @@ /* eslint-disable max-lines */ /* eslint-disable max-lines-per-function */ import { Button, Typography } from '@equinor/eds-core-react'; -import { useMutation } from '@tanstack/react-query'; -import { useState } from 'react'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { AddAnalogueModelMetadataCommandForm, @@ -10,6 +10,8 @@ import { AnalogueModelDetail, AnalogueModelMetadataService, AnalogueModelSourceType, + GenerateThumbnailCommand, + JobsService, JobStatus, MetadataDto, UpdateAnalogueModelCommandBody, @@ -22,6 +24,7 @@ import { StratigrapicGroups } from '../../../components/StrategraphicColumn/Stra import { useFetchModel } from '../../../hooks/useFetchModel'; import { EditNameDescription } from '../EditNameDescription/EditNameDescription'; import * as Styled from './ModelMetadataView.styled'; +import { getAnalogueModelImage } from '../../../api/custom/getAnalogueModelImageById'; export const ModelMetadataView = ({ modelIdParent, @@ -34,6 +37,9 @@ export const ModelMetadataView = ({ modelIdParent ? modelIdParent : undefined, ); const [isAddModelDialog, setAddModelDialog] = useState(false); + + const generateImageRequested = useRef(false); + const { modelId } = useParams(); const defaultMetadata: AnalogueModelDetail = { @@ -54,6 +60,53 @@ export const ModelMetadataView = ({ processingStatus: JobStatus.UNKNOWN, }; + const imageId = data?.data.analogueModelImage?.analogueModelImageId ?? ''; + + const imageRequest = useQuery({ + queryKey: ['analogue-model-image', modelId, imageId], + queryFn: () => getAnalogueModelImage(modelId!, imageId), + enabled: imageId !== '', + }); + + const generateThumbnail = useMutation({ + mutationFn: (requestBody: GenerateThumbnailCommand) => { + return JobsService.postApiJobsComputeThumbnailGen(requestBody); + }, + }); + + const generateThumbnailOnLoad = useCallback( + async (modelId: string) => { + if (modelId) { + const res = await generateThumbnail.mutateAsync({ + modelId: modelId, + }); + return res; + } + }, + [generateThumbnail], + ); + + useEffect(() => { + if ( + modelId && + data && + !isLoading && + data?.data?.analogueModelImage === null && + generateImageRequested.current === false && + data.data.isProcessed + ) { + generateImageRequested.current = true; + generateThumbnailOnLoad(modelId); + } + }, [ + data, + isLoading, + data?.data?.analogueModelImage, + modelId, + generateThumbnailOnLoad, + data?.data.isProcessed, + ]); + function toggleEditMetadata() { setAddModelDialog(!isAddModelDialog); } @@ -174,7 +227,6 @@ export const ModelMetadataView = ({ return res; } }; - const deleteGdeRow = async (gdeGroupId: string) => { if (modelId) { const res = await deleteGdeCase.mutateAsync({ @@ -194,32 +246,65 @@ export const ModelMetadataView = ({ if (isLoading || !data?.success) return

Loading ...

; return ( - + {uploadingProgress === undefined && ( - - <> - {data.data.description && ( - - {data.data.description} - - )} - + + + Description + <> + {data.data.description && ( + + {data.data.description} + + )} + - - - - + + + + {imageRequest.data && data.data && ( + + + {data.data.name} + + )} + + {data.data.isProcessed && + !imageRequest.data && + generateImageRequested.current && ( +
+ + We are generating image for this analogue model + + + Please come back to this page in a couple of minutes + +
+ )} + {!data.data.isProcessed && ( +
+ + Cannot generate picture for unprocessed model. + + + If processing failed, delete this model and reupload again. + Else, wait. + +
+ )} +
+ )} {uploadingProgress !== undefined && uploadingProgress >= 0 && diff --git a/src/hooks/useFetchImageMetadata.tsx b/src/hooks/useFetchImageMetadata.tsx new file mode 100644 index 00000000..5f52f5b5 --- /dev/null +++ b/src/hooks/useFetchImageMetadata.tsx @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMsal } from '@azure/msal-react'; +import { useParams } from 'react-router-dom'; +import { useAccessToken } from './useAccessToken'; +import { AnalogueModelImagesService } from '../api/generated'; + +export const useFetchImageMetadata = (imageId: string) => { + const { modelId = '' } = useParams(); + const { instance, accounts } = useMsal(); + const token = useAccessToken(instance, accounts[0]); + + const query = useQuery({ + queryKey: ['analogue-model-image-metadata', modelId, imageId], + queryFn: () => + AnalogueModelImagesService.getApiAnalogueModelsImagesMetadata( + modelId, + imageId, + ), + enabled: !!token && modelId !== '', + }); + + return query; +};