Skip to content

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:

  1. Displays Processed Metrics: Shows calculated values like land area, building area, volume, and height derived from the geographic data.
  2. Displays Metadata: Presents extracted information, such as EXIF tags from an uploaded photo.
  3. 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.
  4. 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} &#40;{unit}&#41;: {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:

  1. The tags prop, holding the metadata.
  2. A useMemo hook called tagLists. This hook calculates the list of React elements to display based on the tags data. It only re-calculates this list if the tags prop changes, which is an optimization technique in React.
  3. Inside useMemo, it iterates through the keys (tag names) of the tags object.
  4. For each tag, it creates a ListItem component using @mui/material.
  5. 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').
  6. Finally, the component renders a <Drawer> component for the metadata panel. The open prop controls whether the drawer is visible, driven by the extractedDrawerOpen state variable (which is managed by the parent component and passed down). The list items generated by tagLists are rendered inside this drawer's List 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:

  1. The LOAD GEOJSON button. As discussed in Chapter 2: User Input Handling (Files & Interaction), this uses a hidden <input type="file">. The onChange event on that input triggers onFileSelectHandler. This handler reads the file content and then calls the handleFileRead prop function (provided by the parent) to actually process the GeoJSON data.
  2. The LAZ file dropdown. This is a Material UI Select component. When the user selects a file name from the list, the onChange event triggers onLazChangeHandler. This handler finds the corresponding file object from the component's internal lazList state (which is populated by fetching a list of files from a server, see useEffect in the component) and then calls the onLazChange prop function (provided by the parent) with the selected file object. The parent component will then likely fetch the content of this LAZ file.
  3. 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.gl PointCloudLayer 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. Its open state (useState(true)) determines if it's fully visible or collapsed to a narrow bar.
  • The metadata Drawer is controlled by the extractedDrawerOpen prop, which is managed by the parent component (MapResultView or MapShowcaseView) 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 within BuildingAttributes 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.

Next Chapter: Main App 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