Skip to content

Chapter 5: ConnectToLog#

Welcome back! In the previous chapters, we've built up our streetscape.gl application: we learned about the XVIZ Loader Interface (Chapter 1), used the LogViewer to display the 3D scene (Chapter 2), added the PlaybackControl to navigate through time (Chapter 3), and used the XVIZPanel to show dashboard widgets defined in the log metadata (Chapter 4).

Notice a pattern? All these components – LogViewer, PlaybackControl, XVIZPanel, and even the smaller widgets inside XVIZPanel – need to do two things:

  1. Get specific pieces of data or state from the XVIZ Loader instance (like the current timestamp, the current frame data, the buffered ranges, or metadata).
  2. Automatically update themselves whenever that data or state changes in the loader (which happens when the log plays, seeks, or loads new data).

Manually subscribing to the loader's update event using log.on('update', ...) and then calling this.setState(...) within each component that needs log data can quickly become repetitive and complex. You have to manage subscriptions in componentDidMount and componentWillUnmount, and figure out exactly which pieces of state changed.

This is the problem the connectToLog utility function solves.

Think of connectToLog as a smart helper that handles the subscription process for you. It connects a standard React component to an XVIZ Loader instance and automatically provides the component with specific pieces of log data or state as props. Whenever the loader's state changes, connectToLog ensures your component receives the updated props and re-renders.

It's like setting up a personal assistant for your component: you tell the assistant (which is connectToLog) what information you need from the log, and the assistant keeps an eye on the log, fetching the latest information whenever it changes and delivering it directly to your component's props.

What is connectToLog?#

connectToLog is a Higher-Order Component (HOC). Don't worry too much about the technical term "Higher-Order Component" if you're new to React patterns. For beginners, just think of it as a wrapper function that takes a React component and returns a new, "connected" component.

This new, connected component is aware of the XVIZ Loader instance it receives as a prop and knows how to extract data from it and update the original component when necessary.

Its main job is to make it easy for any React component to receive data from an XVIZ Loader without dealing with manual event subscriptions.

Using connectToLog: The Basics#

To use connectToLog, you need:

  1. A React component that you want to give access to log data. This component will be the one that actually renders UI based on the data.
  2. A function that tells connectToLog which data you want from the log.

Here's the basic structure:

import { connectToLog } from 'streetscape.gl';

// 1. Your original React component
// This component receives log data as props
function MyDataDisplay(props) {
  // Use the props provided by connectToLog
  const { someLogData, anotherValue } = props;

  return (
    <div>
      {/* Display your data here */}
      <p>Some log data: {someLogData}</p>
      <p>Another value: {anotherValue}</p>
    </div>
  );
}

// 2. A function that tells connectToLog how to get data from the log
// This function receives the log instance and any other props passed to the wrapper
function getTheStuffINeed(log, ownProps) {
  if (!log) {
    // Always handle cases where the log isn't available yet
    return {};
  }
  // This is where you call methods on the log loader
  const dataForMyComponent = log.someGetterMethod(); // e.g., log.getCurrentTime()
  const anotherValueFromLog = log.anotherGetter(); // e.g., log.getLogEndTime()

  // Return an object: its keys/values become props for MyDataDisplay
  return {
    someLogData: dataForMyComponent,
    anotherValue: anotherValueFromLog
  };
}

// 3. Use connectToLog to wrap your component
const ConnectedDataDisplay = connectToLog({
  Component: MyDataDisplay,      // The component to wrap
  getLogState: getTheStuffINeed  // The function that gets data
});

// Now you can use ConnectedDataDisplay in your app, passing the log instance
// <ConnectedDataDisplay log={myLogLoaderInstance} />

The connectToLog function takes an options object with two main properties:

  • Component: This is the React component you want to wrap. This component should be designed to receive the log data it needs as props.
  • getLogState: This is a function that you write. Its job is to receive the log instance (and any other props passed to the wrapper component, called ownProps) and return an object. The properties of this object will be merged into the props passed to your original Component.

When the log's state changes (internally, connectToLog listens to the log's state updates), the getLogState function is called again with the latest log instance, and your Component is re-rendered with the new data returned by getLogState.

Example: Displaying the Current Timestamp#

Let's create a simple component that just displays the current timestamp from the log.

First, our basic component that receives timestamp as a prop:

// src/components/CurrentTimestamp.js
import React from 'react';

// This component doesn't know *how* the timestamp is updated,
// it just expects it as a prop.
function CurrentTimestampDisplay(props) {
  const { timestamp } = props;

  // Format the timestamp nicely, handle loading state
  const displayTime = timestamp !== undefined && timestamp !== null
    ? new Date(timestamp * 1000).toLocaleTimeString() // XVIZ timestamps are often seconds
    : 'Loading...';

  return (
    <div style={{ margin: '10px', fontWeight: 'bold' }}>
      Current Time: {displayTime}
    </div>
  );
}
This is just a standard React component. It expects a timestamp prop.

Next, we define the getLogState function that will get the timestamp from the log loader:

// src/components/CurrentTimestamp.js (continued)
import { connectToLog } from 'streetscape.gl';

// This function tells connectToLog how to map log state to props
const getTimestampFromLog = (log, ownProps) => {
  // Ensure log is available before calling methods
  if (!log) {
    return {}; // Return empty object if log is null/undefined
  }
  // Get the current time from the log loader
  const currentTime = log.getCurrentTime();

  // Return an object where the key 'timestamp' will become a prop
  return {
    timestamp: currentTime
  };
};
This getTimestampFromLog function simply calls log.getCurrentTime() and returns an object { timestamp: ... }.

Finally, we use connectToLog to wrap CurrentTimestampDisplay using our getTimestampFromLog function:

// src/components/CurrentTimestamp.js (continued)

// Wrap the component to connect it to the log
const ConnectedTimestampDisplay = connectToLog({
  Component: CurrentTimestampDisplay,
  getLogState: getTimestampFromLog
});

export default ConnectedTimestampDisplay;

Now, in our main application component, we can render ConnectedTimestampDisplay and pass our logLoader instance to its log prop:

import React from 'react';
import { LogViewer, PlaybackControl, XVIZFileLoader } from 'streetscape.gl';
import ConnectedTimestampDisplay from './components/CurrentTimestamp'; // Our new component

// Assume logLoader is created and connected as before
const logLoader = new XVIZFileLoader({ /* ... options ... */ });
logLoader.connect();

function App() {
  return (
    <div>
      <div style={{ width: '100%', height: '60vh' }}>
        <LogViewer log={logLoader} />
      </div>
      <div style={{ width: '100%', padding: '0 20px' }}>
        {/* PlaybackControl also takes the log prop */}
        <PlaybackControl log={logLoader} />
        {/* Our new connected component takes the log prop */}
        <ConnectedTimestampDisplay log={logLoader} />
      </div>
    </div>
  );
}

// export default App;

Now, when you run this application: * The ConnectedTimestampDisplay component receives the logLoader instance as a prop. * Internally, connectToLog subscribes to updates from logLoader. * When the log starts or the time changes (e.g., via PlaybackControl), logLoader notifies its subscribers. * connectToLog catches the notification, calls getTimestampFromLog(logLoader, props), gets the new currentTime, and triggers a re-render of CurrentTimestampDisplay with the updated timestamp prop. * CurrentTimestampDisplay then renders the new time.

You've successfully created a component that automatically reacts to log time changes using connectToLog!

This same pattern is used internally by LogViewer, PlaybackControl, XVIZPanel, and its child widgets (_XVIZMetric, _XVIZPlot, etc.) to get the specific data they need from the XVIZ Loader and stay updated.

Under the Hood: How connectToLog Works#

Let's peek behind the curtain to understand how connectToLog achieves this automatic updating.

When you call connectToLog({ Component: MyComponent, getLogState: myGetter }), it doesn't modify MyComponent. Instead, it creates a new React component class, let's call it LogConnectorWrapper.

  1. Mounting: When LogConnectorWrapper is rendered and mounts, it checks if a log prop was provided. If so, it calls a method like log.subscribe(this._onLogUpdate) to register an internal callback function (_onLogUpdate). This tells the XVIZ Loader to notify this wrapper component whenever its state changes.
  2. Log Updates: When the XVIZ Loader's state changes (e.g., new data is buffered, the current time is updated, metadata loads), it internally triggers a notification mechanism. All registered subscribers (like our LogConnectorWrapper) are called.
  3. Wrapper Re-renders: The _onLogUpdate callback inside LogConnectorWrapper receives the notification and triggers a state change within the LogConnectorWrapper component itself (often just incrementing a version counter or setting a flag). This state change tells React to re-render the LogConnectorWrapper.
  4. Calling getLogState: During the LogConnectorWrapper's render method, it calls the getLogState function that you provided, passing the current log instance and the wrapper's own props (ownProps). getLogState fetches the specific data you want from the latest state of the log instance.
  5. Rendering Original Component: The LogConnectorWrapper then renders your original MyComponent, passing down:
    • All the original props that were passed to the wrapper (ownProps).
    • Crucially, the properties returned by your getLogState function.
  6. Component Updates: Your MyComponent receives the new props and re-renders its UI based on the updated data.

This cycle repeats whenever the log instance signals a change.

Here's a simplified sequence diagram:

sequenceDiagram
    participant App as Your App
    participant Wrapper as LogConnectorWrapper (created by connectToLog)
    participant Loader as XVIZ Loader Instance
    participant YourComponent as Your Component<br>(e.g., CurrentTimestampDisplay)

    App->>Wrapper: Render <Wrapper log={loader} ... />
    Wrapper->>Loader: subscribe(this._onLogUpdate)
    Note over Loader: Loader state changes<br>(time, data, etc.)
    Loader->>Wrapper: Call this._onLogUpdate()
    Wrapper->>Wrapper: Trigger internal update/re-render
    Wrapper->>Wrapper: Call getLogState(loader, ownProps)
    Wrapper->>Loader: getCurrentTime() etc.
    Loader-->>Wrapper: Return requested data
    Wrapper->>YourComponent: Render <YourComponent {...dataFromGetLogState} ...ownProps />
    YourComponent->>YourComponent: Update UI with new props

Looking at snippets from the actual connect.js source file (modules/core/src/components/connect.js), we can see this pattern:

// From modules/core/src/components/connect.js (simplified)

export default function connectToLog({getLogState, Component}) {
  class WrappedComponent extends PureComponent { // This is the LogConnectorWrapper
    // ... constructor, propTypes ...

    componentDidMount() {
      const {log} = this.props;
      if (log) {
        log.subscribe(this._update); // Subscribe when mounted
      }
    }

    componentWillReceiveProps(nextProps) {
      const {log} = this.props;
      const nextLog = nextProps.log;
      if (log !== nextLog) { // Handle case where the log instance changes
        if (log) {
          log.unsubscribe(this._update); // Unsubscribe from old log
        }
        if (nextLog) {
          nextLog.subscribe(this._update); // Subscribe to new log
        }
      }
    }

    componentWillUnmount() {
      const {log} = this.props;
      if (log) {
        log.unsubscribe(this._update); // Unsubscribe when unmounted
      }
    }

    _update = logVersion => {
      // This function is called by the log loader.
      // Trigger a state update to force re-render.
      this.setState({logVersion}); // State change forces render
    };

    render() {
      const {log, ...otherProps} = this.props; // Get log prop and other props

      // Call the user's getLogState function to get data
      const logState = log && getLogState(log, otherProps);

      // Render the original component, passing combined props
      return <Component {...otherProps} {...logState} log={log} />;
    }
  }

  return WrappedComponent; // Return the wrapper component
}
This simplified code shows the core logic: subscribing to the log prop, triggering a state update in _update when notified by the log, and calling getLogState in the render method to compute props for the original Component.

This mechanism is fundamental to how many streetscape.gl components function. As we saw in previous chapters, the getLogState functions for LogViewer, PlaybackControl, and XVIZPanel (and its children) retrieve specific data points (currentTime, bufferedRanges, uiConfig, currentFrame, variables, etc.) that are necessary for those components to render the correct information at any given moment in the log.

Conclusion#

connectToLog is a powerful utility in streetscape.gl that simplifies connecting any React component to an XVIZ Loader instance. By handling the subscription lifecycle and automatically mapping data from the log state to component props via the getLogState function, it allows you to easily build custom widgets and components that react dynamically to changes in your log data without complex manual event handling.

Many of the core streetscape.gl components you've already seen, and the ones you'll see next, use connectToLog or a similar pattern internally to stay synchronized with the log.

In the next chapter, we'll finally dive into the XVIZLayer, the deck.gl layer responsible for rendering the rich 3D primitives (points, lines, polygons, models, etc.) from the XVIZ data within the LogViewer. It, too, relies on the XVIZ Loader and mechanisms like the one provided by connectToLog to get the data frame it needs to draw.

XVIZLayer


Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/docs/api-reference/connect-to-log.md), 2(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/docs/developer-guide/state-management.md), 3(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/connect.js), 4(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/index.js)