resium
Version:
React component library for Cesium
326 lines (289 loc) • 10.5 kB
text/typescript
import { Event as CesiumEvent } from "cesium";
import {
useEffect,
useRef,
useImperativeHandle,
useState,
useCallback,
useLayoutEffect,
RefObject,
} from "react";
import { RootComponentInternalProps } from "./component";
import { ResiumContext, useCesium } from "./context";
import { EventManager, eventManagerContextKey, eventNames } from "./EventManager";
import { includes, shallowEquals, isDestroyed, isPromise } from "./util";
export type EventkeyMap<T, P> = { [K in keyof P]?: keyof T };
type CreateReturnType<Element, State = any> = Element | [Element, State] | undefined;
export type Options<Element, Props extends RootComponentInternalProps, State = any> = {
name: string;
create?: (
ctx: ResiumContext,
props: Props,
wrapperRef: HTMLDivElement | null,
) => Promise<CreateReturnType<Element, State>> | CreateReturnType<Element, State>;
destroy?: (
element: Element,
ctx: ResiumContext,
wrapperRef: HTMLDivElement | null,
state?: State,
) => void;
provide?: (
element: Element,
ctx: ResiumContext,
props?: Props,
state?: State,
) => Partial<ResiumContext>;
update?: (
element: Element,
props: Props,
prevProps: Props,
context: ResiumContext,
) => void | Promise<void>;
cesiumProps?: readonly (keyof Props)[];
cesiumReadonlyProps?: readonly (keyof Props)[];
cesiumEventProps?: EventkeyMap<Element, Props>;
otherProps?: readonly (keyof Props)[];
setCesiumPropsAfterCreate?: boolean;
useCommonEvent?: boolean;
useRootEvent?: boolean;
};
export const useCesiumComponent = <Element, Props extends RootComponentInternalProps, State = any>(
{
name,
create,
destroy,
provide,
update,
cesiumReadonlyProps,
cesiumEventProps,
otherProps,
setCesiumPropsAfterCreate,
useCommonEvent,
useRootEvent,
}: Options<Element, Props, State>,
props: Props,
ref: any,
): [Partial<ResiumContext> | undefined, boolean, RefObject<HTMLDivElement | null>] => {
const element = useRef<Element | undefined>(undefined);
const ctx = useCesium();
const provided = useRef<Partial<ResiumContext> | undefined>(provide ? {} : undefined);
const attachedEvents = useRef<{
[key in keyof Element]?: any;
}>({});
const initialProps = useRef<Props>(propsWithChildren(props));
const prevProps = useRef<Props>({} as Props);
const [mounted, setMounted] = useState(false);
const mountedRef = useRef(false);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const stateRef = useRef<State | undefined>(undefined);
const eventManager = ctx?.[eventManagerContextKey];
const mountReadyRef = useRef<Promise<void>>(undefined);
const unmountReadyRef = useRef<Promise<void>>(undefined);
// Update properties
const updateProperties = useCallback(
async (props: Props) => {
if (!element.current) return;
const target: any = element.current;
const propsKeys = Object.keys(props) as (keyof Props)[];
const eventKeys = Object.keys(cesiumEventProps || []) as (keyof Props)[];
const propDiff = propsKeys
.concat(
(Object.keys(prevProps.current) as (keyof Props)[]).filter(k => !propsKeys.includes(k)),
)
.filter(k => prevProps.current[k] !== props[k])
.map(k => [k, prevProps.current[k], props[k]] as [keyof Props, any, any]);
const updatedReadonlyProps: (keyof Props)[] = [];
for (const [k, prevValue, newValue] of propDiff) {
if (cesiumReadonlyProps?.includes(k)) {
updatedReadonlyProps.push(k);
} else if (includes(eventKeys, k)) {
const cesiumKey = cesiumEventProps?.[k] as keyof Element;
const eventHandler = target[cesiumKey];
if (eventHandler instanceof CesiumEvent) {
if (typeof prevValue === "undefined") {
// added
eventHandler.addEventListener(newValue);
attachedEvents.current[cesiumKey] = newValue;
} else if (typeof newValue === "undefined") {
// deleted
eventHandler.removeEventListener(prevValue);
delete attachedEvents.current[cesiumKey];
} else {
// updated
eventHandler.removeEventListener(prevValue);
eventHandler.addEventListener(newValue);
}
}
} else if (k !== "children" && !eventNames.includes(k as any) && !otherProps?.includes(k)) {
target[k] = newValue;
}
}
const em = useRootEvent
? (provided.current?.[eventManagerContextKey] as EventManager | undefined)
: eventManager;
if (useCommonEvent && em && element.current) {
em.setEvents(useRootEvent ? null : element.current, props);
}
if (update && mountedRef.current) {
const maybePromise = update(element.current, props, prevProps.current, ctx);
if (isPromise(maybePromise)) {
await maybePromise;
}
}
prevProps.current = props;
initialProps.current = props;
// Recreate cesium element when any read-only prop is updated
if (mountedRef.current && updatedReadonlyProps.length > 0) {
if (process.env.NODE_ENV !== "production") {
console.warn(
`Warning: <${name}> is recreated because following read-only props have been updated: ${updatedReadonlyProps.join(
", ",
)}`,
);
}
beforeUnmount();
await unmount();
mountReadyRef.current = mount();
}
},
[], // eslint-disable-line react-hooks/exhaustive-deps
);
const mount = useCallback(async () => {
// Wait one tick to resolve Cesium's unmount process.
await new Promise(r => queueMicrotask(() => r(undefined)));
// Initialize cesium element
const maybePromise = create?.(ctx, initialProps.current, wrapperRef.current);
let result: CreateReturnType<Element, State>;
if (isPromise(maybePromise)) {
result = await maybePromise;
} else {
result = maybePromise as CreateReturnType<Element, State>;
}
if (Array.isArray(result)) {
element.current = result[0];
stateRef.current = result[1];
} else {
element.current = result;
}
if (setCesiumPropsAfterCreate) {
await updateProperties(initialProps.current);
} else {
// Attach events
if (element.current && cesiumEventProps) {
const target: any = element.current;
for (const key of Object.keys(initialProps.current) as (keyof Props)[]) {
const eventKey = cesiumEventProps[key];
if (eventKey) {
const e: any = initialProps.current[key];
const eventHandler = target[eventKey];
if (e && eventHandler instanceof CesiumEvent) {
eventHandler.addEventListener(e);
}
}
}
}
prevProps.current = initialProps.current;
}
if (provide && element.current) {
provided.current = {
...ctx,
...provide(element.current, ctx, props, stateRef.current),
};
}
const em = useRootEvent
? (provided.current?.[eventManagerContextKey] as EventManager | undefined)
: eventManager;
if (useCommonEvent && em && element.current) {
em.setEvents(useRootEvent ? null : element.current, initialProps.current);
}
if (!unmountReadyRef.current) {
setMounted(true);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const beforeUnmount = useCallback(() => {
setMounted(false);
mountedRef.current = false;
}, []);
const unmount = useCallback(async () => {
// Wait one tick to resolve Cesium's unmount process.
await new Promise(r => queueMicrotask(() => r(undefined)));
// Wait mount before unmount
if (mountReadyRef.current) {
await mountReadyRef.current;
mountReadyRef.current = undefined;
}
// Destroy cesium element
if (element.current && destroy) {
destroy(element.current, ctx, wrapperRef.current, stateRef.current);
}
const em = useRootEvent
? (provided.current?.[eventManagerContextKey] as EventManager | undefined)
: eventManager;
if (useCommonEvent && em && element.current) {
em.clearEvents(useRootEvent ? null : element.current);
}
// Detach all events
if (element.current && !isDestroyed(element.current)) {
const attachedEventKeys = Object.keys(attachedEvents.current) as (keyof Element)[];
for (const k of attachedEventKeys) {
const eventHandler: any = element.current[k];
eventHandler?.removeEventListener?.(attachedEvents.current[k]);
}
}
attachedEvents.current = {};
provided.current = undefined;
stateRef.current = undefined;
element.current = undefined;
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// To prevent re-execution by hot loader, execute only once
useLayoutEffect(() => {
const run = async () => {
if (unmountReadyRef.current) {
await unmountReadyRef.current;
unmountReadyRef.current = undefined;
}
mountReadyRef.current = mount();
};
run();
return () => {
// Need to update this state before Cesium is updated,
// because Viewer's children must be hidden before it's unmounted.
beforeUnmount();
unmountReadyRef.current = unmount();
};
}, [mount, unmount, beforeUnmount]);
// Update properties of cesium element
useEffect(() => {
const update = async () => {
if (mountReadyRef.current) {
await mountReadyRef.current;
}
const propsWC = propsWithChildren(props);
if (mounted) {
if (!shallowEquals(propsWC, prevProps.current)) {
await updateProperties(propsWC);
ctx.__$internal?.onUpdate?.();
}
} else {
// first time
prevProps.current = propsWC;
initialProps.current = propsWC;
mountedRef.current = true;
}
};
update();
}, [ctx.__$internal, mounted, props, updateProperties]);
// Expose cesium element
useImperativeHandle(
ref,
() => ({
cesiumElement: mounted ? element.current : null,
}),
[mounted],
);
return [provided.current, mounted, wrapperRef];
};
function propsWithChildren<T>(props: T): T {
const { children: _children, ...propsWithoutChildren } = props as any;
return propsWithoutChildren;
}