diff --git a/examples/website/scenegraph/README.md b/examples/website/scenegraph/README.md index cdd9a5ccd5a..e5ff2295859 100644 --- a/examples/website/scenegraph/README.md +++ b/examples/website/scenegraph/README.md @@ -20,7 +20,8 @@ The 3D model is created by `manilov.ap`, modified for this application. See [profile page on sketchfab](https://sketchfab.com/3d-models/boeing747-1a75633f5737462ebc1c7879869f6229), licensed under [Creative Commons](https://creativecommons.org/licenses/by/4.0/). -The real-time flight information is from [Opensky Network](https://opensky-network.org). +The example tries to load live flight data from [Opensky Network](https://opensky-network.org) +and falls back to a bundled snapshot if the API is unavailable. To use your own data, check out the [documentation of ScenegraphLayer](../../../docs/api-reference/mesh-layers/scenegraph-layer.md). diff --git a/examples/website/scenegraph/app.tsx b/examples/website/scenegraph/app.tsx index 4a59d79946d..bc4e0e8fa65 100644 --- a/examples/website/scenegraph/app.tsx +++ b/examples/website/scenegraph/app.tsx @@ -2,23 +2,24 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -/* global fetch, setInterval, clearInterval */ -import React, {useCallback, useEffect, useRef, useState} from 'react'; +/* global fetch */ +import React, {useEffect, useRef, useState} from 'react'; import {createRoot} from 'react-dom/client'; import {Map} from 'react-map-gl/maplibre'; import {DeckGL} from '@deck.gl/react'; import {ScenegraphLayer} from '@deck.gl/mesh-layers'; +import {log} from '@deck.gl/core'; +import sampleData from './all.json'; import type {ScenegraphLayerProps} from '@deck.gl/mesh-layers'; import type {PickingInfo, MapViewState} from '@deck.gl/core'; -// Data provided by the OpenSky Network, https://opensky-network.org +// Live data provided by the OpenSky Network, https://opensky-network.org. +// The API may reject browser requests from other origins, so the bundled +// snapshot below remains the default and fallback data. const DATA_URL = 'https://opensky-network.org/api/states/all'; -// For local debugging -// const DATA_URL = './all.json'; const MODEL_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/scenegraph-layer/airplane.glb'; -const REFRESH_TIME_SECONDS = 60; const DROP_IF_OLDER_THAN_SECONDS = 120; const ANIMATIONS: ScenegraphLayerProps['_animations'] = { @@ -71,14 +72,26 @@ const DATA_INDEX = { CATEGORY: 17 } as const; -async function fetchData(signal: AbortSignal): Promise { - const resp = await fetch(DATA_URL, {signal}); - const {time, states} = (await resp.json()) as {time: number; states: Aircraft[]}; +function normalizeData({time, states}: {time: number; states: Aircraft[]}): Aircraft[] { // make lastContact timestamp relative to response time - for (const a of states) { - a[DATA_INDEX.LAST_CONTACT] -= time; + return states.map(state => { + const aircraft = [...state] as Aircraft; + aircraft[DATA_INDEX.LAST_CONTACT] -= time; + return aircraft; + }); +} + +// https://github.com/visgl/deck.gl/pull/10465 updated the live OpenSky URL, +// but the endpoint still rejects cross-origin browser requests. Start with this +// bundled snapshot so the demo can render immediately and keep it as fallback. +const DATA = normalizeData(sampleData as {time: number; states: Aircraft[]}); + +async function fetchData(signal: AbortSignal): Promise { + const response = await fetch(DATA_URL, {signal}); + if (!response.ok) { + throw new Error(`OpenSky request failed with status ${response.status}`); } - return states; + return normalizeData((await response.json()) as {time: number; states: Aircraft[]}); } function getTooltip({object}: PickingInfo) { @@ -93,22 +106,6 @@ function getTooltip({object}: PickingInfo) { ); } -export function useInterval(callback: () => unknown, delay: number) { - const savedCallback = useRef(callback); - - // Update callback. - useEffect(() => { - savedCallback.current = callback; - }, [callback]); - - // Loop. - useEffect(() => { - savedCallback.current(); // Initial call. - const id = setInterval(() => savedCallback.current(), delay); - return () => clearInterval(id); - }, [delay]); -} - export default function App({ sizeScale = 25, onDataLoad, @@ -118,36 +115,32 @@ export default function App({ onDataLoad?: (count: number) => void; mapStyle?: string; }) { - const [abortController] = useState(new AbortController()); - const dataRef = useRef([]); // Callback requires stable reference to data. - const [, setVersion] = useState(0); // Re-render on data change. - - const sync = useCallback(async () => { - let newData = await fetchData(abortController.signal); - - // In order to keep the animation smooth we need to always return the same - // object at a given index. This function will discard new objects - // and only update existing ones. - if (dataRef.current.length > 0) { - const dataById: Record = {}; - newData.forEach(entry => (dataById[entry[DATA_INDEX.UNIQUE_ID]] = entry)); - newData = dataRef.current.map(entry => dataById[entry[DATA_INDEX.UNIQUE_ID]] || entry); - } + const [data, setData] = useState(DATA); + const onDataLoadRef = useRef(onDataLoad); - dataRef.current = newData; - setVersion(v => v + 1); + useEffect(() => { + onDataLoadRef.current = onDataLoad; + }, [onDataLoad]); - if (onDataLoad) { - onDataLoad(newData.length); - } - }, []); + useEffect(() => { + onDataLoadRef.current?.(data.length); + }, [data]); - useInterval(sync, REFRESH_TIME_SECONDS * 1000); - useEffect(() => () => abortController.abort(), []); + useEffect(() => { + const abortController = new AbortController(); + fetchData(abortController.signal) + .then(setData) + .catch(error => { + if (!abortController.signal.aborted) { + log.warn('Scenegraph example is using its bundled flight snapshot', error)(); + } + }); + return () => abortController.abort(); + }, []); const layer = new ScenegraphLayer({ id: 'scenegraph-layer', - data: dataRef.current, + data, pickable: true, sizeScale, scenegraph: MODEL_URL, @@ -170,9 +163,6 @@ export default function App({ getScale: d => { const lastContact = d[DATA_INDEX.LAST_CONTACT]; return lastContact < -DROP_IF_OLDER_THAN_SECONDS ? [0, 0, 0] : [1, 1, 1]; - }, - transitions: { - getPosition: REFRESH_TIME_SECONDS * 1000 } }); diff --git a/website/src/examples/scenegraph-layer.js b/website/src/examples/scenegraph-layer.js index 13fb084f8c2..002ef745795 100644 --- a/website/src/examples/scenegraph-layer.js +++ b/website/src/examples/scenegraph-layer.js @@ -10,7 +10,7 @@ import App from 'website-examples/scenegraph/app'; import {makeExample} from '../components'; class ScenegraphDemo extends Component { - static title = 'Realtime Flight Tracker'; + static title = 'Flight Tracker'; static code = `${GITHUB_TREE}/examples/website/scenegraph`; @@ -24,7 +24,7 @@ class ScenegraphDemo extends Component { return (

- Data source: + Flight data: The OpenSky Network