Skip to content

Chapter 2: User Input Handling (Files & Interaction)#

Welcome back! In Chapter 1: Application Views, we learned how our application organizes its different screens or "rooms" using the LAYOUT enum and how the MainView component switches between them based on the application's state.

But how does the application know when to switch views? How does it get the data it needs to display on those views, like the photo the user wants to analyze or the GeoJSON file showing building outlines?

This is where User Input Handling comes in. It's the crucial part of the application that listens to the user, understands what they want to do, and gets the necessary information from them.

What's the Problem?#

Our app needs to be interactive. Users don't just passively look at it; they need to:

  • Upload photos they've taken.
  • Load geographic data files (like GeoJSON or LAZ).
  • Click buttons to trigger actions (like processing data or switching views).
  • Maybe even use keyboard shortcuts for quick navigation.

Without handling these inputs, the app would just sit there, static and unresponsive. We need mechanisms to capture these actions and data from the user.

What is User Input Handling?#

User Input Handling is the code responsible for:

  1. Listening: Detecting when a user interacts with the application (clicks, types, drags a file, etc.).
  2. Receiving Data: Getting the specific information from the input (the file content, the text typed, which button was clicked).
  3. Processing: Doing initial work with the received data (like reading a file, extracting simple details).
  4. Reacting: Triggering changes in the application state or calling other functions based on the input.

Think of it like the app having eyes, ears, and hands. It uses its "senses" (event listeners) to notice user actions, its "hands" (code) to grab the data, and its "brain" (processing logic) to figure out what that means and what to do next.

Handling File Uploads: Our Core Use Case#

A key part of this project is analyzing photos, which means the user needs a way to get their photos into the application. Let's focus on how the app handles uploading an image file, which is done primarily in the PhotoView (Chapter 1: Application Views briefly mentioned this view).

When you're on the LAYOUT.PHOTO screen, you see buttons like "Take a picture" and "Upload image". Clicking these buttons lets you select a file from your device.

How does this work in the code?

The File Input Element#

In web development, the standard way to let a user upload a file is using a specific HTML input element: <input type="file">.

You can see this in the src/views/photo-view.tsx file within the PhotoView component:

// Inside PhotoView component in src/views/photo-view.tsx (simplified)

// ... styled components and imports ...

const VisuallyHiddenInput = styled("input")({
  clip: "rect(0 0 0 0)",
  clipPath: "inset(50%)",
  height: 1,
  overflow: "hidden",
  position: "absolute",
  bottom: 0,
  left: 0,
  whiteSpace: "nowrap",
  width: 1,
});

// ... inside the component's return JSX ...

<Button
  component="label" // Makes the button behave like a label for the input
  variant="contained"
  startIcon={<CameraAlt />}
>
  {/* The actual file input, hidden visually but triggered by clicking the Button */}
  <VisuallyHiddenInput
    onChange={onImageChangeHandler} // This function is called when a file is selected
    type="file"
    accept="image/*" // Only accept image files
    capture // Hint to use camera on mobile
  />
</Button>

Here's what's happening:

  1. We use a MUI (@mui/material) Button component.
  2. We set component="label". This is a common pattern: clicking the <label> element automatically clicks the associated <input> element. We associate them by putting the input inside the Button which acts as the label due to the prop.
  3. We include the <input type="file"> element inside the Button.
  4. We use a styled component VisuallyHiddenInput to make the actual file input box invisible (opacity: 0, position: absolute, etc.) because the native file input often doesn't look great and is hard to style. The user interacts with the nice-looking Button.
  5. The crucial part is the onChange prop on the <input>. This prop is set to a function called onImageChangeHandler. This function is an event handler. The browser automatically calls onImageChangeHandler whenever the user successfully selects a file using the file dialog triggered by this input.

The Event Handler: onImageChangeHandler#

When onChange is triggered, the browser provides an event object to the onImageChangeHandler function. This event object contains information about what happened, including the file(s) the user selected.

Look at the onImageChangeHandler function in src/views/photo-view.tsx:

// Inside PhotoView component in src/views/photo-view.tsx

const onImageChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
  // Check if files were selected
  if (!e.target.files || e.target.files.length === 0) {
    onImageChange(undefined); // No file selected, clear previous
    return;
  }
  // Get the first selected file (assuming single file upload here)
  const selectedFile = e.target.files[0];

  // Call the prop function passed from MainView
  onImageChange(selectedFile);
};

This is a very simple handler:

  1. It receives the event object (e).
  2. It checks e.target.files. This is a FileList object containing the files selected by the user. If the user cancels the dialog, this might be empty.
  3. It grabs the first file from the list (e.target.files[0]). For this feature, we only handle one image at a time.
  4. It calls a function onImageChange and passes the selectedFile to it.

Notice onImageChangeHandler doesn't do much processing itself. Its main job is to get the file data from the event and then pass it along to another function (onImageChange). Where does onImageChange come from?

Passing Data Up: Props#

In React, data often flows down from parent components to child components via props. To pass data or trigger actions up from a child to a parent, the parent passes a function down as a prop, and the child calls that function when something happens.

In our case, MainView renders PhotoView and passes the onImageChange function down as a prop:

// Inside MainView component in src/views/main-view.tsx (simplified)

// ... state definitions like const [selectedImg, setSelectedImg] = useState<File | null | undefined>(undefined); ...

return (
  // ... other elements ...
  {activeLayout === LAYOUT.PHOTO && (
    <PhotoView
      // ... other props ...
      onImageChange={(result) => setSelectedImg(result)} // MainView passes this function down
      // ... other props ...
    />
  )}
  // ... other elements ...
);

Here, MainView defines a function (result) => setSelectedImg(result) which updates the selectedImg state variable. It passes this function down to PhotoView via the onImageChange prop.

So, when the user selects a file in PhotoView, onImageChangeHandler is called. It gets the file and calls props.onImageChange(selectedFile). This executes the function defined in MainView, which updates MainView's selectedImg state.

What Happens Next? Processing and State Updates#

Once MainView's state (specifically selectedImg) is updated, React re-renders MainView, and subsequently, PhotoView. The PhotoView component's rendering logic (return JSX) uses the updated state (previewImg, which is derived from selectedImg elsewhere in MainView). This is how the application displays a preview of the uploaded image.

But what about extracting metadata like EXIF tags? This processing happens after the file is selected and the state is updated. The logic for reading the file's contents, extracting EXIF, and updating other state variables (tags) also lives in MainView or is triggered by the state change there. We'll dive deeper into data processing in a later chapter (Chapter 8: Geographic Data Processing). For now, just know that selecting the file is the input that kicks off this processing chain.

Here's a simplified flow of the photo upload input:

sequenceDiagram
    participant User
    participant PhotoView
    participant MainView
    participant Metadata Extraction (later chapter)

    User->PhotoView: Clicks "Upload image" button
    PhotoView->Browser: Triggers file dialog via hidden input
    User->Browser: Selects file and confirms
    Browser->PhotoView: Triggers 'change' event on input
    PhotoView->PhotoView: Calls onImageChangeHandler
    onImageChangeHandler->PhotoView: Gets selected file
    onImageChangeHandler->MainView: Calls onImageChange prop function with file
    MainView->MainView: Updates selectedImg state (triggers re-render)
    MainView->Metadata Extraction (later chapter): Triggers metadata extraction and GeoJSON processing
    MainView->MainView: Updates tags and geo state
    MainView->PhotoView: Renders PhotoView with previewImg and tags props
    PhotoView-->>User: Displays image preview and "Metadata results" button

This diagram shows how the user's single action (selecting a file) flows through the components, updates the application's central state (MainView), and triggers subsequent actions (like processing).

Other Types of Input#

File uploads are just one type of user input. The application handles others using similar principles (event listeners and handler functions):

  • Button Clicks: When you click the "Showcase" button in PhotoView, its onClick prop (onShowcaseClick) is called. This prop function, passed from MainView, updates the activeLayout state to LAYOUT.SHOWCASE, switching the view (as discussed in Chapter 1: Application Views).

    // Inside PhotoView component in src/views/photo-view.tsx (simplified)
    <Button
      // ... props ...
      onClick={onShowcaseClick} // Calls the prop function when clicked
    >
      Showcase
    </Button>
    
    The onShowcaseClick prop in PhotoViewProps expects a function that takes no arguments and returns nothing (() => void). In MainView, this is connected to () => setActiveLayout(LAYOUT.SHOWCASE).

  • Selecting from Dropdowns: In the BuildingAttributes component (used in the map views), there's a dropdown to select LAZ files. The Select component has an onChange prop.

    // Inside BuildingAttributes component in src/components/building-attributes.tsx (simplified)
    <Select
      // ... other props ...
      value={lazFile?.name || ""} // The currently selected value
      label="Laz file"
      onChange={onLazChangeHandler} // Handler for selection change
    >
      {/* ... MenuItem options ... */}
    </Select>
    
    The onLazChangeHandler function gets an event object containing the new selected value. It then finds the corresponding file object and calls props.onLazChange (passed from MainView or MapResultView/MapShowcaseView) to update the application state about which LAZ file is selected.

  • Keyboard Shortcuts: The useKeyboard hook (src/hooks/useKeyboard.ts) demonstrates handling keyboard input.

    // Inside src/hooks/useKeyboard.ts (simplified)
    import { useCallback, useEffect } from "react";
    
    export const useKeyboard = (
      setView: (veiw: "firstPerson" | "map" | "orthographic") => void
    ) => {
      const handleKeyPress = useCallback(
        (event: KeyboardEvent) => {
          // Check the key pressed
          switch (event.key.toUpperCase()) {
            case "T":
              setView("orthographic"); // Call the function passed in
              break;
            case "P":
              setView("map");
              break;
            case "D":
              setView("firstPerson");
              break;
            default:
          }
        },
        [setView] // Recreate handler if setView changes
      );
    
      useEffect(() => {
        // Add event listener when the component mounts
        window.addEventListener("keypress", handleKeyPress);
    
        return () => {
          // Clean up the event listener when the component unmounts
          window.removeEventListener("keypress", handleKeyPress);
        };
      }, [handleKeyPress]); // Re-run effect if handleKeyPress changes
    };
    
    This hook uses useEffect to add a global keypress event listener to the browser window. When a key is pressed, handleKeyPress is called. It checks event.key and calls the setView function (passed into the hook) with the appropriate view type string ("orthographic", "map", "firstPerson"). This setView function would likely be one that updates a state variable controlling the 3D view mode in a component like MapResultView or MapShowcaseView.

In all these cases, the core pattern is similar: an event occurs on a specific element (or the window), a predefined handler function is called, the handler function processes the immediate input data, and then it often calls another function (usually a prop function from a parent component) to update application state or trigger more complex logic elsewhere.

Conclusion#

Handling user input is how our application becomes interactive. We learned that different types of input (file selection, button clicks, key presses, dropdowns) are captured using browser events and processed by corresponding event handler functions. These handlers typically extract the necessary data and then communicate it upwards, often by calling prop functions provided by parent components (like MainView). This flow of information leads to updates in the application's state, triggering re-renders and subsequent processing or view changes.

Now that we know how to get data and commands from the user, the next step is to understand how to show them complex geographic data.

Next Chapter: 3D Geographic Visualization


Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/components/bottom-navigation.tsx), 2(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/components/building-attributes.tsx), 3(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/hooks/useKeyboard.ts), 4(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/views/photo-view.tsx)