Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task/wg 381 assets view image video no asset #292

Merged
merged 11 commits into from
Dec 17, 2024
101 changes: 100 additions & 1 deletion react/src/__fixtures__/featuresFixture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Point } from 'geojson';
import { FeatureCollection, AssetType } from '@hazmapper/types';
import { FeatureCollection, AssetType, Feature } from '@hazmapper/types';

export const featureCollection: FeatureCollection = {
type: 'FeatureCollection',
Expand Down Expand Up @@ -188,3 +188,102 @@ export const featureCollectionWithDuplicateImages: FeatureCollection = {
},
],
};

export const mockImgFeature: Feature = {
id: 10000,
project_id: 100,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-97.726075437, 30.389785554],
},
properties: {
path: '/test/test/Photo 4.jpg',
title: 'Photo 4',
units: '',
format: 'jpg',
system: 'project-11111-242ac11c-0001-012',
_author: 'Test',
basePath: '/test/test',
data_type: 'image',
_timestamp: 1558449670.0902429,
misc_notes: '',
description: 'Words',
geoid_model: '',
geolocation: [
{
course: -1,
altitude: 236.7534637451172,
latitude: 30.38978555407587,
longitude: -97.72607543696425,
},
],
coord_system: '',
geodetic_datum: '',
vertical_datum: '',
instrument_type: 'other',
_test_asset_uuid: '0007766-226F-4A04-93ED-EC769630D372',
coord_ref_system: '',
data_hazard_type: 'other',
coord_system_epoch: '',
referenced_data_links: [],
geodetic_datum_realization: '',
_test_parent_collection_uuid: 'DB66DAEA-2B36-4D20-83C1-K9876C0',
instrument_manufacturer_and_model: '',
},
styles: {},
assets: [
{
id: 1412157,
path: '925/test-86cc-4ae2-8820-f30353b6c714.jpeg',
uuid: 'test-86cc-4ae2-8820-f30353b6c714',
asset_type: 'image',
original_path: 'project-test-242ac11c-0001-012/test/test/Photo 4.jpg',
original_name: null,
display_path: 'project-test-242ac11c-0001-012/test/test/Photo 4.jpg',
},
],
};
export const mockPointFeature: Feature = {
id: 100001,
project_id: 100,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [173.043228, -42.619206],
},
properties: {
name: 'IMG_0198.JPG',
color: '#ff0000',
label: 'IMG_0198.JPG',
opacity: 1,
fillColor: '#ff0000',
description:
'<img src="https://test.com"/>\n<html xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:msxsl="urn:schemas-microsoft-com:xslt">\nOblique dextral fault scarp along pre-existing fold(?) scarp\n<head>\n\n<META http-equiv="Content-Type" content="text/html">\n\n<meta http-equiv="content-type" content="text/html; charset=UTF-8">\n\n</head>\n\n<body style="margin:0px 0px 0px 0px;overflow:auto;background:#FFFFFF;">\n\n<table style="font-family:Arial,Verdana,Times;font-size:12px;text-align:left;width:100%;border-collapse:collapse;padding:3px 3px 3px 3px">\n\n<tr style="text-align:center;font-weight:bold;background:#9CBCE2">\n\n<td>IMG_0198.JPG</td>\n\n</tr>\n\n<tr bgcolor="#9CBCE2">\n\n<td>\n\n<table style="font-family:Arial,Verdana,Times;font-size:12px;text-align:left;width:100%;border-spacing:0px; padding:3px 3px 3px 3px"></table>\n\n</td>\n\n</tr>\n\n<tr>\n\n<td>\n\n<table style="font-family:Arial,Verdana,Times;font-size:12px;text-align:left;width:100%;border-spacing:0px; padding:3px 3px 3px 3px">\n\n<tr>\n\n<td>Path</td>\n\n<td>E:\\EH717\\New_Zealand\\112116\\11_21_2016\\Stahl\\TS06\\IMG_0198.JPG</td>\n\n</tr>\n\n<tr bgcolor="#D4E4F3">\n\n<td>Name</td>\n\n<td>IMG_0198.JPG</td>\n\n</tr>\n\n<tr>\n\n<td>DateTime</td>\n\n<td>2016:11:21 16:12:23</td>\n\n</tr>\n\n<tr bgcolor="#D4E4F3">\n\n<td>Direction</td>\n\n<td>247.482353</td>\n\n</tr>\n\n</table>\n\n</td>\n\n</tr>\n\n</table>\n\n</body>\n\n</html>',
},
styles: null,
assets: [],
};

export const mockVideoFeature: Feature = {
id: 100002,
project_id: 100,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-97.725663, 30.390238],
},
properties: {},
styles: null,
assets: [
{
id: 60933,
path: '1027/935152cc-dba1-4cb6-9efd-82036a6446ac.mp4',
uuid: '935152cc-dba1-4cb6-9efd-82036a6446ac',
asset_type: 'video',
original_path: '/test/test/Video 1.mov',
original_name: null,
display_path: '/test/test/Video 1.mov',
},
],
};
85 changes: 85 additions & 0 deletions react/src/components/AssetDetail/AssetDetail.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.root {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
}

.topSection {
flex: 0 0 auto;
padding: 10px;
display: flex;
text-align: center;
justify-content: space-between;
align-items: center;
font-size: large;
}

.middleSection {
flex: 1 1 auto;
overflow: hidden;
min-height: 0;
display: flex;
text-align: center;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
}
.middleSection > div {
flex: 0 1 auto;
overflow: auto;
}
.assetContainer {
display: flex;
align-content: center;
justify-content: center;
}
.assetContainer > * {
max-width: 100%;
}
.bottomSection {
display: block;
flex: 0 0 auto;
overflow-x: hidden;
justify-items: flex-end;
align-items: flex-end;
width: 100%;
}
.metadataTable {
flex-grow: 1;
width: 100%;
}
.metadataTable table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin: 5px;
padding: 5px;
}
.metadataTable thead,
th {
background: #d0d0d0;
}
.metadataTable tbody {
display: block;
max-height: 300px;
overflow-y: auto;
scroll-padding: 5px;
}
.metadataTable tr {
display: table;
width: 100%;
table-layout: fixed;
text-overflow: ellipsis;
overflow: hidden;
white-space: wrap;
max-height: 30px;
}
.metadataTable td {
word-wrap: break-word;
word-break: break-word;
white-space: normal;
text-overflow: clip;
}
28 changes: 28 additions & 0 deletions react/src/components/AssetDetail/AssetDetail.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { render } from '@testing-library/react';
import AssetDetail from './AssetDetail';
import { mockImgFeature } from '@hazmapper/__fixtures__/featuresFixture';

jest.mock('@hazmapper/hooks', () => ({
useFeatureSelection: jest.fn(),
useAppConfiguration: jest.fn().mockReturnValue({
geoapiUrl: 'https://example.com/geoapi',
}),
}));

describe('AssetDetail', () => {
const AssetModalProps = {
onClose: jest.fn(),
selectedFeature: mockImgFeature,
isPublicView: false,
};

it('renders all main components', () => {
const { getByText } = render(<AssetDetail {...AssetModalProps} />);
// Check for title, button, and tables
expect(getByText('Photo 4.jpg')).toBeDefined();
expect(getByText('Download')).toBeDefined();
expect(getByText('Metadata')).toBeDefined();
expect(getByText('Geometry')).toBeDefined();
});
});
162 changes: 162 additions & 0 deletions react/src/components/AssetDetail/AssetDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { Suspense } from 'react';
import _ from 'lodash';
import { useAppConfiguration } from '@hazmapper/hooks';
import { FeatureTypeNullable, Feature, getFeatureType } from '@hazmapper/types';
import { FeatureIcon } from '@hazmapper/components/FeatureIcon';
import { Button, LoadingSpinner, SectionMessage } from '@tacc/core-components';
import styles from './AssetDetail.module.css';

type AssetModalProps = {
onClose: () => void;
selectedFeature: Feature;
isPublicView: boolean;
};

const AssetDetail: React.FC<AssetModalProps> = ({
selectedFeature,
onClose,
isPublicView,
}) => {
const config = useAppConfiguration();
const geoapiUrl = config.geoapiUrl;

const featureSource: string =
geoapiUrl + '/assets/' + selectedFeature?.assets?.[0]?.path;

const fileType = getFeatureType(selectedFeature);

const AssetRenderer = React.memo(
({
type,
source,
nathanfranklin marked this conversation as resolved.
Show resolved Hide resolved
}: {
type: string | undefined;
source: string | undefined;
}) => {
switch (type) {
case 'image':
return <img src={source} alt="Asset" loading="lazy" />;
case 'video':
return (
<video src={source} controls preload="metadata">
<track kind="captions" />
</video>
);
case 'point_cloud':
/*TODO Add pointcloud */
return <div> source={source}</div>;
case 'questionnaire':
/*TODO Add questionnaire */
return <div> source={source}</div>;
default:
return null;
}
}
);
AssetRenderer.displayName = 'AssetRenderer';

return (
<div className={styles.root}>
<div className={styles.topSection}>
<FeatureIcon featureType={fileType as FeatureTypeNullable} />
{selectedFeature?.assets?.length > 0
? selectedFeature?.assets.map((asset) =>
// To make sure fileTree name matches title and catches null
asset.display_path
? asset.display_path.split('/').pop()
: asset.id
? asset.id
: selectedFeature.id
)
: selectedFeature?.id}
<Button type="link" iconNameAfter="close" onClick={onClose}></Button>
</div>
<div className={styles.middleSection}>
sophia-massie marked this conversation as resolved.
Show resolved Hide resolved
{fileType ? (
<>
<Suspense fallback={<LoadingSpinner />}>
<div className={styles.assetContainer}>
<AssetRenderer type={fileType} source={featureSource} />
</div>
</Suspense>
<Button /*TODO Download Action */>Download</Button>
sophia-massie marked this conversation as resolved.
Show resolved Hide resolved
</>
) : (
<>
<SectionMessage type="info">Feature has no asset.</SectionMessage>
{!isPublicView && (
<Button type="primary" /* TODO Add asset to a feature */>
Add asset from DesignSafe
</Button>
)}
</>
)}
</div>
<div className={styles.bottomSection}>
<div className={styles.metadataTable}>
<table>
<thead>
<tr>
<th colSpan={2}>Metadata</th>
sophia-massie marked this conversation as resolved.
Show resolved Hide resolved
</tr>
</thead>
<tbody>
{selectedFeature?.properties &&
Object.keys(selectedFeature.properties).length > 0 ? (
Object.entries(selectedFeature.properties)
.filter(([key]) => !key.startsWith('_hazmapper'))
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) // Alphabetizes metadata
.map(([propKey, propValue]) => (
<tr key={propKey}>
<td>{_.startCase(propKey)}</td>
<td>
{propKey.startsWith('description') ? (
<code>{propValue}</code>
) : (
_.trim(JSON.stringify(propValue), '"')
)}
</td>
</tr>
))
) : (
<tr>
<td colSpan={2}>There are no metadata properties.</td>
</tr>
)}
</tbody>
</table>
<table>
<thead>
<tr>
<th colSpan={2}>Geometry</th>
</tr>
</thead>
<tbody>
{selectedFeature?.geometry &&
Object.entries(selectedFeature.geometry).map(
([propKey, propValue]) =>
propValue &&
propValue !== undefined &&
propValue.toString().trim() !== '' &&
propValue.toString() !== 'null' && (
<tr key={propKey}>
<td>{_.trim(_.startCase(propKey.toString()), '"')}</td>
<td>
{' '}
{Array.isArray(propValue) && propValue.length === 2
? `Latitude: ${propValue[0].toString()},
Longitude: ${propValue[1].toString()}`
: _.trim(JSON.stringify(propValue), '"')}
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
</div>
);
};

export default AssetDetail;
1 change: 1 addition & 0 deletions react/src/components/AssetDetail/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './AssetDetail';
Loading
Loading