Skip to content

Commit

Permalink
Task/wg 381 assets view image video no asset (#292)
Browse files Browse the repository at this point in the history
* task/WG-381-Assets-View-Image-Video-No-Asset
- Adds place and styles for panel on MapProject page

* - Changes useCurrentFeature hook so that it picks
up the latest query accurately
- Creates asset detail component with styles
- Creates asset detail placeholders for questionnaire and pointcloud
- Need to add meaningful test

* - Addresses requested changes.
TODO: Updated geometry to be handled like angular

* Requested changes 12.17
- Removes code now that selectedFeature is passed in as a prop

* Adds an asset fixture

* - Fixes autocomplete error in test

* Add current features hook and extend selected feature hook

* - Linting

* - Removes unused test render

* Catches weird cases where display_path does not
exist but file tree uses asset.id before selectedFeature.id
- See v3_PROD_Hazmapper_2024-08-07_Test map to check
  • Loading branch information
sophia-massie authored Dec 17, 2024
1 parent 88d89fc commit 04a764f
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 15 deletions.
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,
}: {
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}>
{fileType ? (
<>
<Suspense fallback={<LoadingSpinner />}>
<div className={styles.assetContainer}>
<AssetRenderer type={fileType} source={featureSource} />
</div>
</Suspense>
<Button /*TODO Download Action */>Download</Button>
</>
) : (
<>
<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>
</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

0 comments on commit 04a764f

Please sign in to comment.