Skip to content

Chapter 5: Main App Logic#

Welcome back to the tutorial! In the previous chapters, we've built up individual pieces of our application: * Chapter 1: Application Views showed how we divide our app into different screens like Login, Photo Upload, and Map views. * Chapter 2: User Input Handling (Files & Interaction) explained how we capture user actions, especially file uploads. * Chapter 3: 3D Geographic Visualization introduced how we use Deck.gl and Mapbox to display geographic data in 3D. * Chapter 4: Building Information Display focused on showing detailed information and data loading controls in a sidebar panel.

Now, the big question is: How do all these parts work together? Who decides when to switch views? Who takes the uploaded photo and tells the visualization component to show the corresponding building? Who manages the data that flows between these different pieces?

This is the job of the Main App Logic.

What is Main App Logic?#

Think of your application as an orchestra. You have different sections (the views, the visualization, the info panel, the input handlers). Each section is good at its specific task, but they need someone to coordinate them, tell them when to play, and ensure they're using the same sheet music (the application data).

The Main App Logic acts as the conductor of this orchestra. It's the central control unit that:

  1. Manages the State: Keeps track of important information that the whole application might need, such as:
    • Which view is currently active (Chapter 1).
    • The photo file the user selected (Chapter 2).
    • The processed geographic data (like building GeoJSON) related to the photo.
    • Any extracted metadata (like EXIF tags).
    • Loading status (is the app currently busy processing?).
    • Any error messages to show.
    • Which LAZ file is selected and whether it should be drawn.
    • The user's login token.
  2. Reacts to Input: Listens for signals from user input components (like "a file was selected" or "this button was clicked") and decides what actions to take.
  3. Orchestrates Data Flow: Gets data from input handlers or API calls and passes it down to the components responsible for display (Chapter 3, Chapter 4).
  4. Coordinates Actions: Triggers data processing (Chapter 8), API fetching (Chapter 7), and view switches based on the current state and user input.

In essence, the Main App Logic connects all the pieces we've discussed so far, making the application dynamic and responsive.

Where Does the Main App Logic Live?#

In our React application, the main app logic is primarily located within a single, high-level component: MainView (src/views/main-view.tsx).

Why put it here? Because MainView is the component that renders the different application views (LoginView, PhotoView, MapResultView, MapShowcaseView) using conditional rendering based on the activeLayout state (Chapter 1). Since it's the parent of these views, it's in a perfect position to:

  • Hold state that needs to be shared or influence multiple views.
  • Define functions that handle cross-cutting concerns (like what happens after any file is uploaded).
  • Pass data and these handler functions down as props to its child view components.

Let's look at the structure of MainView and how it handles its conductor role.

Core Elements of MainView as the Conductor#

MainView uses standard React hooks like useState and useEffect to manage the application's state and react to changes.

Managing Application State (useState)#

MainView holds the central pieces of data using useState.

// Inside src/views/main-view.tsx (simplified)
import React, { useState } from "react";
import { LAYOUT } from "../types/layout"; // Import view types

export const MainView = () => {
  // Which screen is currently visible? (Starts at Login)
  const [activeLayout, setActiveLayout] = useState(LAYOUT.LOGIN);

  // The image file the user selected
  const [selectedImg, setSelectedImg] = useState<File | null | undefined>(undefined);

  // Extracted metadata (EXIF tags) from the image
  const [tags, setTags] = useState<any>();

  // Processed geographic data (GeoJSON, camera info, metrics)
  const [geo, setGeo] = useState<any>();

  // Is the app currently loading/processing?
  const [loading, setLoading] = useState<boolean>(false);

  // Any error messages to display
  const [errMsg, setErrMsg] = useState<any>();

  // The currently selected LAZ file from the list
  const [lazFile, setLazFile] = useState<null | NginxFile>(null); // Assuming NginxFile type

  // Should the selected LAZ file be drawn?
  const [drawLaz, setDrawLaz] = useState<boolean>(false);

  // User's authentication token
  const [bearerToken, setBearerToken] = useState<string>('');

  // ... other state variables ...
};

These useState calls define the key pieces of information MainView needs to keep track of to manage the application. When any of these state variables change (e.g., setSelectedImg is called with a new file, or setGeo is called with processed data), MainView re-renders, and this updated state is passed down to the appropriate child components.

Reacting to State Changes (useEffect)#

Some actions in the application don't happen directly because of a user click, but rather because a piece of state has changed. For example, when a user selects a file, we don't immediately process it; we update the selectedImg state. Reacting to this state update is done using the useEffect hook.

MainView uses useEffect for things like:

  • Processing the selectedImg when it changes.
  • Creating a temporary URL (previewImg) to display the selected image.
  • Fetching data or updating other state based on changes.
// Inside src/views/main-view.tsx (simplified)
// ... other useState definitions ...

export const MainView = () => {
  // ... state definitions ...

  // Effect 1: When selectedImg state changes, handle the image processing
  useEffect(() => {
    if (!selectedImg) { // Do nothing if no image is selected (e.g., cleared)
      return;
    }
    // Call the function to process the image (extract EXIF, fetch building data)
    handleImage(selectedImg);
    // This effect depends on `selectedImg`. If selectedImg changes, re-run this effect.
  }, [selectedImg]);

  // Effect 2: When selectedImg state changes, create an object URL for preview
  useEffect(() => {
    if (!selectedImg) { // If no image, clear the preview URL
      setPreviewImg(null);
      return;
    }
    // Create a temporary URL for the browser to display the image
    const objectUrl = URL.createObjectURL(selectedImg as any);
    setPreviewImg(objectUrl);

    // Cleanup function: When the component unmounts or selectedImg changes again,
    // revoke the URL to free up memory.
    return () => URL.revokeObjectURL(objectUrl);
    // This effect depends on `selectedImg`.
  }, [selectedImg]);

  // ... other effects, handler functions, and render logic ...
};

These useEffect hooks are listening for changes in their "dependency arrays" ([selectedImg]). When selectedImg changes (because the user selected a file and setSelectedImg was called), both of these effects run, triggering subsequent actions like processing the image and creating a preview.

Handling User Input and Orchestrating Actions#

As we saw in Chapter 2: User Input Handling (Files & Interaction), input events are often handled by functions defined in the child components (like onImageChangeHandler in PhotoView). However, these handlers typically just extract the raw input (like the File object) and then call a prop function provided by MainView. This allows MainView to centralize the logic that reacts to the input.

Let's trace the photo upload flow from the user selecting a file to seeing the result on the map:

  1. User selects a file: In PhotoView, the onChange event on the hidden file input triggers onImageChangeHandler.
  2. PhotoView calls MainView's handler: onImageChangeHandler gets the selected File object and calls the onImageChange prop function, which is (result) => setSelectedImg(result) in MainView.
  3. MainView updates state: setSelectedImg(selectedFile) is called. This updates MainView's selectedImg state.
  4. useEffect reacts to state change: The useEffect that depends on selectedImg runs, calling handleImage(selectedFile).
  5. handleImage starts processing: This function (defined in MainView) sets loading to true, uses ExifReader to get tags (Chapter 8), updates the tags state, and opens the metadata drawer.
  6. handleImage triggers API call: If GPS tags are found, handleImage calls getPolygon(lat, lon, altitude, direction), another function defined in MainView.
  7. getPolygon fetches data: getPolygon sets loading to true again, calls fetchBuilding (Chapter 7) with the extracted GPS data.
  8. getPolygon updates state and switches view: When fetchBuilding returns data (containing GeoJSON, metrics, etc.), getPolygon calls setGeo(data) and importantly, setActiveLayout(LAYOUT.RESULT). It then sets loading back to false.
  9. MainView re-renders: Because activeLayout and geo state have changed, MainView re-renders.
  10. Conditional rendering switches view: The conditional rendering logic now sees activeLayout === LAYOUT.RESULT is true and renders MapResultView.
  11. Data flows down: MainView passes the updated geo (containing GeoJSON, metrics), tags, previewImg, lazFile, drawLaz, and relevant handler functions (onLazChangeHandler, drawLazHandler, etc.) down as props to MapResultView.
  12. MapResultView displays results: MapResultView uses these props. For instance, it passes the geo data to a helper function (createBuilding) to create Deck.gl layers (Chapter 3), passes the layers and view state to DeckglWrapper for visualization, and passes metrics, tags, and file handlers to BuildingAttributes for the info display (Chapter 4).

Here's a simplified sequence diagram for this flow:

sequenceDiagram
    participant User
    participant PhotoView
    participant MainView
    participant Processing/API (handleImage, getPolygon, fetchBuilding)
    participant MapResultView

    User->PhotoView: Selects file
    PhotoView->MainView: Calls onImageChange prop (setSelectedImg)
    MainView->MainView: Updates selectedImg state
    MainView->MainView: useEffect triggered by selectedImg change
    MainView->Processing/API: Calls handleImage(selectedFile)
    Processing/API->MainView: Updates tags state (setTags)
    Processing/API->Processing/API: Extracts GPS, Calls getPolygon
    Processing/API->Processing/API: Calls fetchBuilding(GPS data)
    Processing/API-->Processing/API: Receives GeoJSON/metrics data
    Processing/API->MainView: Calls setGeo(data) and setActiveLayout(LAYOUT.RESULT)
    MainView->MainView: State updates trigger re-render
    MainView->MapResultView: Renders MapResultView with props (geo, tags, etc.)
    MapResultView-->>User: Displays map, 3D building, info panel

This diagram illustrates how MainView sits in the middle, receiving input, triggering background processes/API calls, updating its central state, and then re-rendering the appropriate child views with the necessary data.

Passing Data Down and Handlers Up#

The core pattern enabling this flow is passing data down the component tree via props and passing functions down via props for child components to call when they need to communicate up or trigger an action in the parent.

Let's see how MainView passes props to MapResultView:

// Inside src/views/main-view.tsx (simplified)
// ... state and handler function definitions ...

return (
  <Box sx={{ display: "flex", height: "100vh" }} ref={ref}>
    {/* ... other global UI like CssBaseline ... */}

    {/* Conditional Rendering */}
    {activeLayout === LAYOUT.RESULT && (
      <MapResultView
        geo={geo}                         {/* Pass geo data down */}
        view={view}                       {/* Pass current camera view type */}
        imageUrl={previewImg}             {/* Pass image preview URL */}
        drawLaz={drawLaz}                 {/* Pass state for drawing LAZ */}
        lazFile={lazFile}                 {/* Pass selected LAZ file */}
        tags={tags}                       {/* Pass extracted metadata */}
        previewImg={previewImg}           {/* Pass image preview URL (redundant prop in snippet?) */}
        extractedDrawerOpen={extractedDrawerOpen} {/* Pass state for metadata drawer */}

        // Pass handler functions down as props for MapResultView or its children to call
        onLazChange={onLazChangeHandler}       {/* Handler for LAZ file selection */}
        drawLaz_={drawLazHandler}              {/* Handler to trigger LAZ drawing */}
        onImageChange={(result) => setSelectedImg(result)} {/* Handler for image upload from inside MapResultView */}
        onShowcaseClick={() => setActiveLayout(LAYOUT.SHOWCASE)} {/* Handler to switch to Showcase view */}
        setExtractedDrawerOpen={setExtractedDrawerOpen} {/* Setter for metadata drawer state */}
      />
    )}

    {/* ... other views and global components ... */}
  </Box>
);

This snippet from MainView's return block shows it rendering MapResultView only when activeLayout is LAYOUT.RESULT. It then explicitly passes each piece of relevant state (geo, view, tags, etc.) and each relevant function (onLazChangeHandler, drawLazHandler, setSelectedImg, setActiveLayout, setExtractedDrawerOpen) as props.

MapResultView (and components it renders like BuildingAttributes or DeckglWrapper) receives these props and uses them:

  • geo is used to create Deck.gl layers (Chapter 3) and metrics (Chapter 4).
  • tags are displayed in the metadata drawer (Chapter 4).
  • onLazChangeHandler is passed down to the BuildingAttributes component's LAZ dropdown (Chapter 4). When the user selects a file there, BuildingAttributes calls this prop function, which then updates the lazFile state in MainView.

This constant flow of data down and action calls up is the hallmark of how components communicate in React and how MainView maintains control as the central logic unit.

Coordinating Global UI#

MainView also manages elements that appear across multiple views or are global to the application, such as:

  • The BottomNav component, which allows switching between views. MainView passes the activeLayout and onLayoutChange handler to it.
  • The Snackbar for displaying messages (errMsg).
  • The Backdrop and CircularProgress for showing a loading spinner (loading state).
  • The InfoButton and InfoModal.

By managing the state (activeLayout, errMsg, loading, infoOpen) for these components in MainView, the conductor can ensure they are displayed correctly based on the overall application status, regardless of which main content view (PhotoView, MapResultView, etc.) is currently active.

Conclusion#

The Main App Logic, centered in the MainView component, is the heart of our application. It acts as the conductor, managing the overall state, receiving inputs from user interaction components (Chapter 2), triggering background processes and API calls (Chapter 7, Chapter 8), deciding which view to display (Chapter 1), and passing data and necessary functions down to the visualization (Chapter 3) and information display (Chapter 4) components.

By centralizing this coordination in MainView using useState, useEffect, and the pattern of passing props down and calling prop functions up, we keep the application organized and make the flow of data and control clear.

Now that we understand how the application logic orchestrates the different parts, the next step is to look at the fundamental data structures, or Data Models, that represent the information flowing through the system.

Next Chapter: Data Models


Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/App/App.tsx), 2(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/views/main-view.tsx)