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:
- 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). - 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:
- 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.
- 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 thelog
instance (and any other props passed to the wrapper component, calledownProps
) and return an object. The properties of this object will be merged into the props passed to your originalComponent
.
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>
);
}
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
};
};
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
.
- Mounting: When
LogConnectorWrapper
is rendered and mounts, it checks if alog
prop was provided. If so, it calls a method likelog.subscribe(this._onLogUpdate)
to register an internal callback function (_onLogUpdate
). This tells theXVIZ Loader
to notify this wrapper component whenever its state changes. - 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 ourLogConnectorWrapper
) are called. - Wrapper Re-renders: The
_onLogUpdate
callback insideLogConnectorWrapper
receives the notification and triggers a state change within theLogConnectorWrapper
component itself (often just incrementing a version counter or setting a flag). This state change tells React to re-render theLogConnectorWrapper
. - Calling
getLogState
: During theLogConnectorWrapper
'srender
method, it calls thegetLogState
function that you provided, passing the currentlog
instance and the wrapper's own props (ownProps
).getLogState
fetches the specific data you want from the latest state of thelog
instance. - Rendering Original Component: The
LogConnectorWrapper
then renders your originalMyComponent
, passing down:- All the original props that were passed to the wrapper (
ownProps
). - Crucially, the properties returned by your
getLogState
function.
- All the original props that were passed to the wrapper (
- 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
}
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.
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)