Chapter 4: Building Information Display#
Welcome back! In Chapter 3: 3D Geographic Visualization, we explored how we use libraries like Deck.gl and Mapbox GL JS to render buildings, photo locations, and other geographic data in an interactive 3D environment.
Seeing a building in 3D on a map is great, but to truly understand it, we need more information. How tall is it? What's its area? Can we see the original photo's details or maybe a dense point cloud scan of the structure?
This is where the Building Information Display comes in.
What's the Problem?#
When we process data about a building β whether it's from a photo, a GeoJSON file, or a point cloud scan β we calculate valuable statistics and extract important metadata.
- We might calculate the building's footprint area, total land area, height, or even volume.
- We might extract location data or camera information from a photo's EXIF tags.
- We need to provide a way for the user to load more related data, like different GeoJSON outlines or supplementary LAZ point cloud files for the same location.
Just showing the 3D model isn't enough; the user needs a dedicated space to see these details and interact with related data files. We need a part of the user interface that acts like a dashboard for the building information.
What is Building Information Display?#
In this project, the Building Information Display refers to a specific UI panel or section dedicated to showing detailed attributes and calculated metrics about a building or geographic area that is currently being visualized.
This panel serves several key purposes:
- Displays Processed Metrics: Shows calculated values like land area, building area, volume, and height derived from the geographic data.
- Displays Metadata: Presents extracted information, such as EXIF tags from an uploaded photo.
- Provides Data Loading Controls: Offers buttons or dropdowns to load additional data files (like GeoJSON or LAZ) that might be related to the current view.
- Organizes Information: Gathers various pieces of data about the building into one accessible location.
Think of it as the building's "information card" or "spec sheet" that pops up alongside the 3D view.
The Core Component: BuildingAttributes
#
The main component responsible for creating this information panel is BuildingAttributes
(src/components/building-attributes.tsx
). This component is designed to be flexible enough to be used in different views that need to show building details, such as the MapResultView
(for a single processed result) and potentially the MapShowcaseView
(though its implementation might differ slightly or show aggregate data).
Let's look at how a parent component, like MapResultView
(Chapter 1: Application Views), uses the BuildingAttributes
component:
// Inside MapResultView component in src/views/map-result-view.tsx (simplified)
// ... state and other handler functions ...
return (
// ... other UI elements ...
<Box sx={{ display: "flex", height: "100vh" }}>
{/* The BuildingAttributes panel component */}
<BuildingAttributes
geojsonFileContents={geo?.geojsonFileContents} // Processed GeoJSON data
metrics={metrics} // Calculated metrics (area, volume, etc.)
lazFile={lazFile} // Currently selected LAZ file
handleFileRead={handleFileRead} // Function to load GeoJSON
previewImg={previewImg} // Preview image URL
onImageChange={onImageChangeHandler} // Handler for image upload
tags={tags} // Extracted metadata (EXIF)
onShowcaseClick={() => setActiveLayout(LAYOUT.SHOWCASE)} // Handler to switch view
setExtractedDrawerOpen={setExtractedDrawerOpen} // State setter for metadata drawer
extractedDrawerOpen={extractedDrawerOpen} // State for metadata drawer
drawLaz_={drawLazHandler} // Handler to draw LAZ data
onLazChange={onLazChangeHandler} // Handler for LAZ selection dropdown
/>
{/* The DeckglWrapper component for 3D visualization (from Chapter 3) */}
<DeckglWrapper
// ... props for map/3D view ...
/>
</Box>
// ... other UI elements ...
);
As you can see, MapResultView
passes a lot of data and functions down to BuildingAttributes
via props. This is the standard React pattern: the parent component manages the core application state and logic, and passes the necessary pieces down to its child components to display and interact with.
Displaying Calculated Metrics#
One of the primary functions of BuildingAttributes
is to show the calculated metrics like area, volume, and height.
These metrics are calculated elsewhere in the application (we'll cover data processing in a later chapter, Chapter 8: Geographic Data Processing) and stored in a state variable in MapResultView
(or similar parent component), likely using the Metrics
interface:
// src/types/metrics.ts
export interface Metrics {
landArea: number;
buildingArea: number;
volume: number;
buildingHeight: number;
}
This Metrics
object, containing numerical values, is passed to BuildingAttributes
via the metrics
prop.
Inside BuildingAttributes
, these values are then displayed using a small, reusable component called MetricDisplay
(src/components/metric-display/metric-display.tsx
).
Here's the simple MetricDisplay
component:
// src/components/metric-display/metric-display.tsx
import Typography from "@mui/material/Typography";
import { ComputedMetric } from "../../types/metric";
// This interface defines the data expected by MetricDisplay
// src/types/metric.ts
export interface ComputedMetric {
label: string; // e.g., "Land Area"
unit: string; // e.g., "m2"
value: number; // e.g., 150.75
}
const MetricDisplay = ({ label, unit, value }: ComputedMetric): JSX.Element => {
return (
<Typography gutterBottom data-testid="metric-display">
{label} ({unit}): {parseFloat(value.toFixed(2))}
</Typography>
);
};
export default MetricDisplay;
This component is very straightforward. It takes an object matching the ComputedMetric
interface (a label
, a unit
, and a value
) and simply formats them into a string like "Land Area (m2): 150.75" using MUI's Typography
component. It rounds the value to two decimal places using toFixed(2)
and parseFloat
.
Now, let's see how BuildingAttributes
uses MetricDisplay
multiple times to show all the metrics:
// Inside BuildingAttributes component in src/components/building-attributes.tsx (simplified)
interface BuildingAttributesProps {
// ... other props ...
metrics: Metrics; // This is where the metrics data comes in
// ... other props ...
}
export const BuildingAttributes = ({
// ... other destructured props ...
metrics, // Destructure the metrics prop
// ... other destructured props ...
}: BuildingAttributesProps) => {
// Destructure individual metric values from the metrics object
const { landArea, buildingArea, volume, buildingHeight } = metrics;
// ... other state, effects, and handlers ...
return (
<>
<StyledDrawer variant="permanent" open={open}>
{/* ... drawer header and other content ... */}
<List dense={true}>
{open && (
<div
// ... styling ...
>
{/* ... file loading sections ... */}
<Box sx={{ mt: 4, mb: 4 }}>
<Typography id="input-slider" variant="h6" gutterBottom>
Statistiques {/* French for Statistics */}
</Typography>
{/* Use MetricDisplay for each metric */}
<MetricDisplay
value={landArea}
unit="m2"
label="Land Area"
/>
<MetricDisplay
value={buildingArea}
unit="m2"
label="Building Area"
/>
{/* Note: building area is listed twice in the provided code, likely a typo */}
<MetricDisplay
value={buildingArea} // This one seems redundant based on provided code
unit="m2"
label="Building Floor Area"
/>
<MetricDisplay value={volume} unit="m3" label="Volume" />
<MetricDisplay
value={buildingHeight}
unit="m"
label="Building Height"
/>
</Box>
{/* ... other content ... */}
</div>
)}
</List>
{/* ... drawer toggle button ... */}
</StyledDrawer>
{/* ... SecondaryDrawer and Extracted Metadata Drawer ... */}
</>
);
};
The BuildingAttributes
component takes the metrics
object from its props, destructures it to get individual values (landArea
, buildingArea
, etc.), and then renders a separate <MetricDisplay>
component for each one, passing the value, desired unit, and label. This makes the code clean and reusable for displaying any single metric.
Displaying Extracted Metadata#
Another piece of information displayed is extracted metadata, primarily from the uploaded image's EXIF tags.
The metadata is processed elsewhere (Chapter 8: Geographic Data Processing) and passed to BuildingAttributes
via the tags
prop. The tags
data structure is defined using the Tag
interface and a Record
:
// Simplified structure based on BuildingAttributesProps
interface Tag {
description?: string; // The value of the tag (e.g., "NIKON D750")
// ... other potential tag properties ...
}
// The tags prop is a map where keys are tag names (e.g., "Make")
// and values are either a single Tag object or an array of Tag objects.
interface BuildingAttributesProps {
// ... other props ...
tags: Record<string, Tag[] | Tag>; // Metadata tags
// ... other props ...
}
Inside BuildingAttributes
, this tags
data is processed and rendered within a separate Drawer
(the "Extracted Metadata" drawer). The rendering logic uses useMemo
to efficiently generate a list of ListItem
components for display.
// Inside BuildingAttributes component in src/components/building-attributes.tsx (simplified)
interface Tag {
description?: string;
}
interface BuildingAttributesProps {
// ... other props ...
tags: Record<string, Tag[] | Tag>; // The metadata comes in via this prop
// ... other props ...
}
export const BuildingAttributes = ({
// ... other destructured props ...
tags, // Destructure the tags prop
// ... other destructured props ...
}: BuildingAttributesProps) => {
// ... state and other handlers ...
// Memoize the list items to avoid re-creating them unnecessarily
const tagLists = useMemo(() => {
if (!tags) {
return null; // Don't render anything if no tags
}
// Iterate over the keys (tag names) of the tags object
return Object.keys(tags).map((keyName: any, i: any) => (
<ListItem key={i}> {/* Unique key for each list item */}
<ListItemText
primary={
// Display the description(s) for the tag
Array.isArray(tags[keyName]) // Check if it's an array of tags
? (tags[keyName] as Tag[])
.map((item: any) => item.description)
.join(", ") // Join descriptions if it's an array
: (tags[keyName] as Tag)?.description || "-" // Otherwise, get the single description
}
secondary={keyName} // Display the tag name as secondary text
/>
</ListItem>
));
}, [tags]); // Re-run this only if the 'tags' prop changes
return (
<>
{/* ... StyledDrawer (main panel) ... */}
{/* ... SecondaryDrawer ... */}
{/* The Drawer specifically for Extracted Metadata */}
<Drawer
anchor={"left"} // Position on the left
variant="persistent" // Stays open until explicitly closed
onClose={toggleExtractedDrawer(false)} // Handler for closing
open={extractedDrawerOpen} // Controlled by state
>
{/* ... Drawer header ... */}
<List
dense={true}
sx={{ width: 300, maxWidth: 300, bgcolor: "background.paper" }}
>
{/* Render the list items generated by useMemo */}
{tagLists}
</List>
</Drawer>
</>
);
};
This snippet shows:
- The
tags
prop, holding the metadata. - A
useMemo
hook calledtagLists
. This hook calculates the list of React elements to display based on thetags
data. It only re-calculates this list if thetags
prop changes, which is an optimization technique in React. - Inside
useMemo
, it iterates through the keys (tag names) of thetags
object. - For each tag, it creates a
ListItem
component using@mui/material
. ListItemText
is used to display the tag's value (primary
) and its name (secondary
). It handles both cases where a tag might have a single value or multiple values (e.g., multiple lenses listed under 'Lens').- Finally, the component renders a
<Drawer>
component for the metadata panel. Theopen
prop controls whether the drawer is visible, driven by theextractedDrawerOpen
state variable (which is managed by the parent component and passed down). The list items generated bytagLists
are rendered inside this drawer'sList
component.
This structure keeps the metadata display logic contained within a specific, collapsible part of the UI, allowing users to view details without cluttering the main panel.
Data Loading Controls#
Beyond displaying information, BuildingAttributes
also acts as a control panel for loading relevant data files.
// Inside BuildingAttributes component in src/components/building-attributes.tsx (simplified)
interface BuildingAttributesProps {
// ... other props ...
handleFileRead: (
isFileUpload: boolean,
customFileData?: string | ArrayBuffer | null
) => void; // Function to handle GeoJSON file reading
onLazChange: (url: NginxFile) => void; // Function called when LAZ file dropdown changes
drawLaz_: () => void; // Function to trigger drawing the selected LAZ file
lazFile: NginxFile | null; // Currently selected LAZ file (for dropdown value)
lazList: NginxFile[]; // List of available LAZ files (fetched elsewhere)
}
export const BuildingAttributes = ({
// ... other destructured props ...
handleFileRead, // Destructure the file reading handler
onLazChange, // Destructure the LAZ change handler
drawLaz_, // Destructure the LAZ drawing handler
lazFile, // Destructure the current LAZ file state
// Note: lazList state is managed internally in this component based on provided code
}: BuildingAttributesProps) => {
// ... state (including lazList using useState) ...
// Handler for selecting a GeoJSON file
const onFileSelectHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const { name } = e.target.files[0];
// ... validation logic ...
const fileReader = new FileReader();
// Call the handleFileRead prop function when file reading is done
fileReader.onloadend = () => handleFileRead(true, fileReader.result);
fileReader.readAsText(e.target.files[0]);
// ... update file name state ...
}
};
// Handler for changing the selection in the LAZ file dropdown
const onLazChangeHandler = (event: SelectChangeEvent) => {
// Find the selected file object from the list
const file = lazList.find((file) => file.name === event.target.value);
if (file) {
// Call the onLazChange prop function with the selected file object
onLazChange(file);
}
};
// ... other handlers ...
return (
<>
<StyledDrawer variant="permanent" open={open}>
{/* ... drawer header ... */}
<List dense={true}>
{open && (
<div
// ... styling ...
>
{/* GeoJSON File Loading Button */}
<div className="button">
<Typography gutterBottom>No file loaded</Typography>
<Button variant="contained" component="label">
LOAD GEOJSON
<input
hidden
accept=".geojson"
onChange={onFileSelectHandler} // Attached to the hidden input
type="file"
/>
</Button>
{/* ... file name display ... */}
</div>
{/* ... sample data links ... */}
<Divider variant="middle" />
{/* LAZ File Selection and Draw Button */}
<Paper
sx={{ flexBasis: "200px", padding: "9px", margin: "20px" }}
>
<Stack
direction={"row"}
spacing={0.5}
sx={{ minWidth: "200px", maxWidth: "calc(100vw - 80px)" }}
>
{/* Button to trigger drawing the selected LAZ file */}
<Button onClick={drawLaz_}>Draw</Button>
{/* Dropdown for selecting LAZ file */}
<FormControl fullWidth size="small">
<InputLabel id="select-laz">Laz file</InputLabel>
<Select
labelId="select-laz"
value={lazFile?.name || ""} // Display name of selected file
label="Laz file"
onChange={onLazChangeHandler} // Attached to the Select
>
{/* Map over lazList state to create dropdown options */}
{lazList.map((file) => (
<MenuItem key={file.name} value={file.name}>
{file.name}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
</Paper>
<Divider variant="middle" />
{/* ... Metrics section ... */}
</div>
)}
</List>
{/* ... drawer toggle button ... */}
</StyledDrawer>
{/* ... SecondaryDrawer and Extracted Metadata Drawer ... */}
</>
);
};
This code highlights:
- The LOAD GEOJSON button. As discussed in Chapter 2: User Input Handling (Files & Interaction), this uses a hidden
<input type="file">
. TheonChange
event on that input triggersonFileSelectHandler
. This handler reads the file content and then calls thehandleFileRead
prop function (provided by the parent) to actually process the GeoJSON data. - The LAZ file dropdown. This is a Material UI
Select
component. When the user selects a file name from the list, theonChange
event triggersonLazChangeHandler
. This handler finds the corresponding file object from the component's internallazList
state (which is populated by fetching a list of files from a server, seeuseEffect
in the component) and then calls theonLazChange
prop function (provided by the parent) with the selected file object. The parent component will then likely fetch the content of this LAZ file. - The Draw button next to the LAZ dropdown. Clicking this button calls the
drawLaz_
prop function (provided by the parent). This function is responsible for taking the currently selected and loaded LAZ file data and creating a Deck.glPointCloudLayer
to visualize it (Chapter 3: 3D Geographic Visualization).
This demonstrates that BuildingAttributes
isn't just passive display; it also incorporates controls for the user to interact with data sources relevant to the building information being shown. It relies on the parent component to provide the functions (handleFileRead
, onLazChange
, drawLaz_
) that perform the actual data processing or rendering updates, keeping BuildingAttributes
focused on the UI and input handling within its panel.
Flow of Information Display#
Here's a simplified flow showing how data gets displayed in the BuildingAttributes
panel:
sequenceDiagram
participant ParentView (e.g., MapResultView)
participant BuildingAttributes
participant MetricDisplay
participant ExtractedMetadataDrawer
ParentView->BuildingAttributes: Pass metrics as prop (e.g., { landArea: 100, ... })
ParentView->BuildingAttributes: Pass tags as prop (e.g., { "Make": { description: "NIKON" } })
ParentView->BuildingAttributes: Pass lazFile, lazList, geojsonFileContents as props
ParentView->BuildingAttributes: Pass handler functions (onLazChange, handleFileRead, etc.) as props
BuildingAttributes->MetricDisplay: Render MetricDisplay for each metric value
MetricDisplay-->>BuildingAttributes: Displays formatted metric string
BuildingAttributes->BuildingAttributes: Check 'extractedDrawerOpen' state
BuildingAttributes->BuildingAttributes: Use useMemo to process 'tags' prop into a list of UI elements
BuildingAttributes->ExtractedMetadataDrawer: Render Drawer with processed tag list
ExtractedMetadataDrawer-->>BuildingAttributes: Displays metadata when open
User->BuildingAttributes: Selects LAZ file from dropdown
BuildingAttributes->BuildingAttributes: Calls onLazChangeHandler
onLazChangeHandler->ParentView: Calls onLazChange prop function with selected file object
Note over ParentView: Parent fetches LAZ data and updates state
User->BuildingAttributes: Clicks LOAD GEOJSON button
BuildingAttributes->BuildingAttributes: Triggers onFileSelectHandler (via hidden input)
onFileSelectHandler->ParentView: Calls handleFileRead prop function with file content
Note over ParentView: Parent processes GeoJSON and updates state (metrics, geojsonFileContents etc.)
This diagram illustrates how the parent view provides the necessary data and functions to BuildingAttributes
. BuildingAttributes
then uses smaller components (MetricDisplay
) or internal logic (useMemo
for tags, event handlers for files) to display the information and handle user interaction within its panel, often calling back to the parent component to trigger application-wide state updates or actions.
Drawer Management#
You might have noticed that the BuildingAttributes
component uses multiple Drawer
components (StyledDrawer
, SecondaryDrawer
, and the metadata Drawer
). These allow the information panel and its sub-panels (like metadata or secondary navigation) to be collapsible or appear/disappear, saving screen space when not needed.
StyledDrawer
controls the main panel that holds the metrics and file loading controls. Itsopen
state (useState(true)
) determines if it's fully visible or collapsed to a narrow bar.- The metadata
Drawer
is controlled by theextractedDrawerOpen
prop, which is managed by the parent component (MapResultView
orMapShowcaseView
) and passed down. This allows the parent view to control when the metadata panel is shown, potentially triggered by user actions elsewhere (though in the provided code, it's toggled by a button withinBuildingAttributes
itself).
This use of drawers is a common UI pattern to organize content and manage screen real estate in web applications.
Conclusion#
In this chapter, we focused on the Building Information Display, the dedicated panel (BuildingAttributes
component) that provides users with detailed metrics, metadata, and data loading controls for the buildings or geographic areas being visualized. We learned how this component receives data and handler functions via props from a parent view component (like MapResultView
). We saw how it uses smaller components like MetricDisplay
to show calculated values and organizes extracted metadata in a separate collapsible drawer. We also revisited how it integrates file input handling (Chapter 2) to allow users to load relevant GeoJSON and LAZ data directly from the panel.
This panel is crucial for making the processed geographic data understandable and actionable for the user, complementing the visual exploration provided by the 3D map.
Next, we'll look at how all these pieces β views, user input, visualization, and information display β come together in the main application logic.
Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/components/building-attributes.tsx), 2(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/components/metric-display/metric-display.tsx), 3(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/metric.ts), 4(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/metrics.ts) Hello Syntax error in text