@mcmhomes/panorama-viewer
Version:
Provides React components to render panoramas.
118 lines (102 loc) • 5.76 kB
JSX
import React from 'react';
import clone from 'clone-deep';
import {PanoramaDefaultLoadingWidget} from './widgets/PanoramaDefaultLoadingWidget.jsx';
import {PanoramaDefaultErrorWidget} from './widgets/PanoramaDefaultErrorWidget.jsx';
import {PanoramaLoaderVariationsRetriever} from './loading/PanoramaLoaderVariationsRetriever.jsx';
import {getCorrectedGivenProps} from './utils/PanoramaPropsParsingUtils.jsx';
import {isHostPrivate, setAnimationFrameTimeoutRemovable, uniqueId} from './utils/PanoramaUtils.jsx';
import {memo, useCallback, useMemo, useState} from './utils/PanoramaUtilsReact.jsx';
/**
* @exports
* @typedef {Object} PanoramaViewerProps
* @property {string} homeId
* @property {string|null} [homeVersion]
* @property {string|null} [host]
* @property {string|null} [styleId]
* @property {string|null} [locationId]
* @property {Object.<string|symbol,string>|null} [skus]
* @property {((error:{canRetry:boolean, retry:()=>void, message:string, reason:string, id:string, data:Object})=>void)|null} [onError]
* @property {((error:{canRetry:boolean, retry:()=>void, message:string, reason:string, id:string, data:Object})=>import('react').ReactNode)|null} [errorWidget]
* @property {(()=>import('react').ReactNode)|null} [loadingWidget]
* @property {number|null} [minFov]
* @property {number|null} [maxFov]
* @property {((aspectRatio:number)=>number)|null} [calculateFov] With this you can override how the field of view is calculated from the aspect ratio, for example: (aspectRatio) => 109.22 - (16.69 * aspectRatio)
* @property {((newFov:number)=>void)|null} [onFovChanged]
* @property {{yaw:number, pitch:number}|null} [initialCameraRotation]
* @property {((newRotation:{yaw:number, pitch:number})=>void)|null} [onCameraRotationChanged]
* @property {number|null} [lookSpeed]
* @property {number|null} [lookSpeedX]
* @property {number|null} [lookSpeedY]
* @property {number|null} [zoomSpeed]
* @property {string|null} [basisTranscoderPath]
*/
/**
* The PanoramaViewer component is the main component for rendering a panoramic home.
*
* @component
* @type {import('react').NamedExoticComponent<PanoramaViewerProps>}
*/
export const PanoramaViewer = memo((props) =>
{
const {homeId:givenHomeId, homeVersion:givenHomeVersion, host:givenHost, styleId:givenStyleId, locationId:givenLocationId, onError, errorWidget, loadingWidget, minFov:givenMinFov, maxFov:givenMaxFov, calculateFov:givenCalculateFov, basisTranscoderPath:givenBasisTranscoderPath, ...other} = props;
const {homeId, homeVersion, host, locationId, styleId, basisTranscoderPath} = useMemo(() => getCorrectedGivenProps({homeId:givenHomeId, homeVersion:givenHomeVersion, host:givenHost, styleId:givenStyleId, locationId:givenLocationId, basisTranscoderPath:givenBasisTranscoderPath}), [givenHomeId, givenHomeVersion, givenHost, givenStyleId, givenLocationId, givenBasisTranscoderPath]);
const {minFov, maxFov} = useMemo(() => getCorrectedGivenProps({minFov:givenMinFov, maxFov:givenMaxFov}), [givenMinFov, givenMaxFov]);
const calculateFov = useMemo(() => givenCalculateFov ?? ((aspectRatio) => (299.36 / (aspectRatio + 2.37)) - 1.35), [givenCalculateFov]);
const [attemptId, setAttemptId] = useState(uniqueId());
const getErrorWidget = useCallback((error) =>
{
error = clone(error, true);
if(error?.canRetry)
{
error.retry = () =>
{
setAttemptId(null);
setAnimationFrameTimeoutRemovable(() => setAttemptId(uniqueId()));
};
}
console.error('[PanoramaViewer] Error: ', error);
onError?.(clone(error, true));
return errorWidget?.(clone(error, true)) ?? (<PanoramaDefaultErrorWidget {...error}/>);
}, [onError, errorWidget, setAttemptId]);
const getLoadingWidget = useCallback(() =>
{
return loadingWidget?.() ?? (<PanoramaDefaultLoadingWidget/>);
}, [loadingWidget]);
const errorComponent = useMemo(() =>
{
if(!isHostPrivate(host))
{
if(!givenHomeId)
{
return getErrorWidget({canRetry:false, id:'missing-home-id', message:'Missing home ID', reason:'the PanoramaViewer component was rendered without being given a valid home ID', data:{homeId:givenHomeId}});
}
if(homeId !== givenHomeId)
{
return getErrorWidget({canRetry:false, id:'invalid-home-id', message:'Invalid home ID: ' + givenHomeId, reason:'the home ID contains invalid characters, only "a-z 0-9 _" is allowed', data:{homeId:givenHomeId}});
}
if(!homeId)
{
return getErrorWidget({canRetry:false, id:'missing-home-id', message:'Missing home ID', reason:'the PanoramaViewer component was rendered without being given a valid home ID', data:{homeId:givenHomeId}});
}
}
if(locationId && (locationId !== givenLocationId))
{
return getErrorWidget({canRetry:false, id:'invalid-location-id', message:'Invalid location ID: ' + givenLocationId, reason:'the location ID contains invalid characters, only "a-z 0-9 _" is allowed', data:{locationId:givenLocationId}});
}
if(styleId && (styleId !== givenStyleId))
{
return getErrorWidget({canRetry:false, id:'invalid-style-id', message:'Invalid style ID: ' + givenStyleId, reason:'the style ID contains invalid characters, only "a-z 0-9 _" is allowed', data:{styleId:givenStyleId}});
}
}, [homeId, givenHomeId, host, locationId, givenLocationId, styleId, givenStyleId, getErrorWidget]);
if(errorComponent)
{
return errorComponent;
}
if(!attemptId)
{
return getLoadingWidget();
}
return (<>
<PanoramaLoaderVariationsRetriever homeId={homeId} homeVersion={homeVersion} host={host} styleId={styleId} locationId={locationId} basisTranscoderPath={basisTranscoderPath} minFov={minFov} maxFov={maxFov} calculateFov={calculateFov} getErrorWidget={getErrorWidget} getLoadingWidget={getLoadingWidget} {...other}/>
</>);
});