From dc02685916bd448ad81efdc3ccea8913fd83641d Mon Sep 17 00:00:00 2001 From: sophia-massie <96220951+sophia-massie@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:43:36 -0600 Subject: [PATCH] Task/wg 403 create asset geometry component (#295) --- .../AssetDetail/AssetDetail.module.css | 14 +-- .../AssetDetail/AssetDetail.test.tsx | 11 +- .../components/AssetDetail/AssetDetail.tsx | 34 +---- .../AssetDetail/AssetGeometry.test.tsx | 28 +++++ .../components/AssetDetail/AssetGeometry.tsx | 118 ++++++++++++++++++ 5 files changed, 166 insertions(+), 39 deletions(-) create mode 100644 react/src/components/AssetDetail/AssetGeometry.test.tsx create mode 100644 react/src/components/AssetDetail/AssetGeometry.tsx diff --git a/react/src/components/AssetDetail/AssetDetail.module.css b/react/src/components/AssetDetail/AssetDetail.module.css index 70037c36..4d8c508c 100644 --- a/react/src/components/AssetDetail/AssetDetail.module.css +++ b/react/src/components/AssetDetail/AssetDetail.module.css @@ -19,7 +19,7 @@ .middleSection { flex: 1 1 auto; overflow: hidden; - min-height: 0; + min-height: 20%; display: flex; text-align: center; flex-direction: column; @@ -41,26 +41,25 @@ } .bottomSection { display: block; - flex: 0 0 auto; + flex: 0 1 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; + margin-top: 5px; + margin-bottom: 5px; padding: 5px; } .metadataTable thead, th { - background: #d0d0d0; + background: var(--global-color-primary--light); + text-emphasis: var(--global-color-primary--dark); } .metadataTable tbody { display: block; @@ -81,5 +80,4 @@ th { word-wrap: break-word; word-break: break-word; white-space: normal; - text-overflow: clip; } diff --git a/react/src/components/AssetDetail/AssetDetail.test.tsx b/react/src/components/AssetDetail/AssetDetail.test.tsx index 5b8daf6d..41293a55 100644 --- a/react/src/components/AssetDetail/AssetDetail.test.tsx +++ b/react/src/components/AssetDetail/AssetDetail.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import AssetDetail from './AssetDetail'; import { mockImgFeature } from '@hazmapper/__fixtures__/featuresFixture'; @@ -10,6 +10,12 @@ jest.mock('@hazmapper/hooks', () => ({ }), })); +jest.mock('./AssetGeometry', () => { + return function AssetGeometry() { + return
Geometry Details
; + }; +}); + describe('AssetDetail', () => { const AssetModalProps = { onClose: jest.fn(), @@ -19,10 +25,11 @@ describe('AssetDetail', () => { it('renders all main components', () => { const { getByText } = render(); + const assetGeometry = screen.getByTestId('asset-geometry'); // Check for title, button, and tables expect(getByText('Photo 4.jpg')).toBeDefined(); expect(getByText('Download')).toBeDefined(); expect(getByText('Metadata')).toBeDefined(); - expect(getByText('Geometry')).toBeDefined(); + expect(assetGeometry).toBeDefined(); }); }); diff --git a/react/src/components/AssetDetail/AssetDetail.tsx b/react/src/components/AssetDetail/AssetDetail.tsx index 957b9743..e40ca64e 100644 --- a/react/src/components/AssetDetail/AssetDetail.tsx +++ b/react/src/components/AssetDetail/AssetDetail.tsx @@ -1,5 +1,6 @@ import React, { Suspense } from 'react'; import _ from 'lodash'; +import AssetGeometry from './AssetGeometry'; import { useAppConfiguration } from '@hazmapper/hooks'; import { FeatureTypeNullable, Feature, getFeatureType } from '@hazmapper/types'; import { FeatureIcon } from '@hazmapper/components/FeatureIcon'; @@ -97,7 +98,9 @@ const AssetDetail: React.FC = ({ - + @@ -125,34 +128,7 @@ const AssetDetail: React.FC = ({ )}
Metadata + Metadata +
- - - - - - - - {selectedFeature?.geometry && - Object.entries(selectedFeature.geometry).map( - ([propKey, propValue]) => - propValue && - propValue !== undefined && - propValue.toString().trim() !== '' && - propValue.toString() !== 'null' && ( - - - - - ) - )} - -
Geometry
{_.trim(_.startCase(propKey.toString()), '"')} - {' '} - {Array.isArray(propValue) && propValue.length === 2 - ? `Latitude: ${propValue[0].toString()}, - Longitude: ${propValue[1].toString()}` - : _.trim(JSON.stringify(propValue), '"')} -
+ diff --git a/react/src/components/AssetDetail/AssetGeometry.test.tsx b/react/src/components/AssetDetail/AssetGeometry.test.tsx new file mode 100644 index 00000000..9d4e67b1 --- /dev/null +++ b/react/src/components/AssetDetail/AssetGeometry.test.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import GeometryAsset from './AssetGeometry'; +import { mockImgFeature } from '@hazmapper/__fixtures__/featuresFixture'; + +jest.mock('@turf/turf', () => ({ + ...jest.requireActual('@turf/turf'), + area: jest.fn(() => 1234.56), + length: jest.fn(() => 5678.9), + bbox: jest.fn(() => [10, 20, 30, 40]), +})); + +jest.mock('@hazmapper/hooks', () => ({ + useFeatureSelection: jest.fn(), +})); + +describe('AssetGeometry', () => { + const GeometryAssetProps = { + selectedFeature: mockImgFeature, + }; + + it('renders geometry for point on an image', () => { + const { getByText } = render(); + expect(getByText('Geometry: Point')).toBeDefined(); + expect(getByText('Latitude')).toBeDefined(); + expect(getByText('Longitude')).toBeDefined(); + }); +}); diff --git a/react/src/components/AssetDetail/AssetGeometry.tsx b/react/src/components/AssetDetail/AssetGeometry.tsx new file mode 100644 index 00000000..7cc7b2fa --- /dev/null +++ b/react/src/components/AssetDetail/AssetGeometry.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import _ from 'lodash'; +import * as turf from '@turf/turf'; +import { Feature, FeatureType } from '@hazmapper/types'; + +interface AssetGeometryProps { + selectedFeature: Feature; +} + +const AssetGeometry: React.FC = ({ selectedFeature }) => { + if (!selectedFeature?.geometry) return null; + + const bbox = + selectedFeature.geometry.type !== 'Point' + ? turf.bbox(selectedFeature) + : null; + + const geometryType = selectedFeature.geometry.type; + + return ( + <> + {geometryType === FeatureType.Point && ( + + + + + + + + + + + + + + + + +
+ Geometry: {_.startCase(selectedFeature.geometry.type)} +
Latitude{turf.bbox(selectedFeature.geometry)[0]}
Longitude{turf.bbox(selectedFeature.geometry)[1]}
+ )} + {(geometryType === FeatureType.Polygon || + geometryType === FeatureType.MultiPolygon) && ( + + + + + + + + + + + + +
+ Geometry: {_.startCase(selectedFeature.geometry.type)} +
Area (m²){turf.area(selectedFeature as turf.Feature).toFixed(2)}
+ )} + {(geometryType === FeatureType.LineString || + geometryType === FeatureType.MultiLineString) && ( + + + + + + + + + + + + +
+ Geometry: {_.startCase(selectedFeature.geometry.type)} +
Length (m){turf.length(selectedFeature as turf.Feature).toFixed(2)}
+ )} + {geometryType !== FeatureType.Point && bbox && ( + + + {selectedFeature.geometry.type === 'GeometryCollection' && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + +
+ Geometry: {_.startCase(selectedFeature.geometry.type)} +
+ Bounding Box +
LatitudeLongitude
Minimum{turf.bbox(selectedFeature.geometry)[1]}{turf.bbox(selectedFeature.geometry)[0]}
Maximum{turf.bbox(selectedFeature.geometry)[3]}{turf.bbox(selectedFeature.geometry)[2]}
+ )} + + ); +}; + +export default AssetGeometry;