@loaders.gl/core
Version:
The core API for working with loaders.gl loaders and writers
215 lines (191 loc) • 7.15 kB
text/typescript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {Loader, LoaderOptions, registerJSModules} from '@loaders.gl/loader-utils';
import {isPureObject, isObject} from '../../javascript-utils/is-type';
import {probeLog, NullLog} from './loggers';
import {DEFAULT_LOADER_OPTIONS, REMOVED_LOADER_OPTIONS} from './option-defaults';
/**
* Global state for loaders.gl. Stored on `globalThis.loaders._state`
*/
type GlobalLoaderState = {
loaderRegistry: Loader[];
globalOptions: LoaderOptions;
};
/**
* Helper for safely accessing global loaders.gl variables
* Wraps initialization of global variable in function to defeat overly aggressive tree-shakers
*/
export function getGlobalLoaderState(): GlobalLoaderState {
// @ts-ignore
globalThis.loaders = globalThis.loaders || {};
// @ts-ignore
const {loaders} = globalThis;
// Add _state object to keep separate from modules added to globalThis.loaders
if (!loaders._state) {
loaders._state = {};
}
return loaders._state;
}
/**
* Store global loader options on the global object to increase chances of cross loaders-version interoperability
* NOTE: This use case is not reliable but can help when testing new versions of loaders.gl with existing frameworks
* @returns global loader options merged with default loader options
*/
export function getGlobalLoaderOptions(): LoaderOptions {
const state = getGlobalLoaderState();
// Ensure all default loader options from this library are mentioned
state.globalOptions = state.globalOptions || {...DEFAULT_LOADER_OPTIONS};
return state.globalOptions;
}
/**
* Set global loader options
* @param options
*/
export function setGlobalOptions(options: LoaderOptions): void {
const state = getGlobalLoaderState();
const globalOptions = getGlobalLoaderOptions();
// @ts-expect-error First param looks incorrect
state.globalOptions = normalizeOptionsInternal(globalOptions, options);
// Make sure any new modules are registered
registerJSModules(options.modules);
}
/**
* Merges options with global opts and loader defaults, also injects baseUri
* @param options
* @param loader
* @param loaders
* @param url
*/
export function normalizeOptions(
options: LoaderOptions,
loader: Loader,
loaders?: Loader[],
url?: string
): LoaderOptions {
loaders = loaders || [];
loaders = Array.isArray(loaders) ? loaders : [loaders];
validateOptions(options, loaders);
return normalizeOptionsInternal(loader, options, url);
}
// VALIDATE OPTIONS
/**
* Warn for unsupported options
* @param options
* @param loaders
*/
function validateOptions(options: LoaderOptions, loaders: Loader[]): void {
// Check top level options
validateOptionsObject(options, null, DEFAULT_LOADER_OPTIONS, REMOVED_LOADER_OPTIONS, loaders);
for (const loader of loaders) {
// Get the scoped, loader specific options from the user supplied options
const idOptions: Record<string, unknown> = ((options && options[loader.id]) || {}) as Record<
string,
unknown
>;
// Get scoped, loader specific default and deprecated options from the selected loader
const loaderOptions = (loader.options && loader.options[loader.id]) || {};
const deprecatedOptions =
(loader.deprecatedOptions && loader.deprecatedOptions[loader.id]) || {};
// Validate loader specific options
// @ts-ignore
validateOptionsObject(idOptions, loader.id, loaderOptions, deprecatedOptions, loaders);
}
}
// eslint-disable-next-line max-params, complexity
function validateOptionsObject(
options: LoaderOptions,
id: string | null,
defaultOptions: Record<string, unknown>,
deprecatedOptions: Record<string, unknown>,
loaders: Loader[]
): void {
const loaderName = id || 'Top level';
const prefix = id ? `${id}.` : '';
for (const key in options) {
// If top level option value is an object it could options for a loader, so ignore
const isSubOptions = !id && isObject(options[key]);
const isBaseUriOption = key === 'baseUri' && !id;
const isWorkerUrlOption = key === 'workerUrl' && id;
// <loader>.workerUrl requires special handling as it is now auto-generated and no longer specified as a default option.
if (!(key in defaultOptions) && !isBaseUriOption && !isWorkerUrlOption) {
// Issue deprecation warnings
if (key in deprecatedOptions) {
probeLog.warn(
`${loaderName} loader option \'${prefix}${key}\' no longer supported, use \'${deprecatedOptions[key]}\'`
)();
} else if (!isSubOptions) {
const suggestion = findSimilarOption(key, loaders);
probeLog.warn(
`${loaderName} loader option \'${prefix}${key}\' not recognized. ${suggestion}`
)();
}
}
}
}
function findSimilarOption(optionKey: string, loaders: Loader[]): string {
const lowerCaseOptionKey = optionKey.toLowerCase();
let bestSuggestion = '';
for (const loader of loaders) {
for (const key in loader.options) {
if (optionKey === key) {
return `Did you mean \'${loader.id}.${key}\'?`;
}
const lowerCaseKey = key.toLowerCase();
const isPartialMatch =
lowerCaseOptionKey.startsWith(lowerCaseKey) || lowerCaseKey.startsWith(lowerCaseOptionKey);
if (isPartialMatch) {
bestSuggestion = bestSuggestion || `Did you mean \'${loader.id}.${key}\'?`;
}
}
}
return bestSuggestion;
}
function normalizeOptionsInternal(
loader: Loader,
options: LoaderOptions,
url?: string
): LoaderOptions {
const loaderDefaultOptions = loader.options || {};
const mergedOptions = {...loaderDefaultOptions};
addUrlOptions(mergedOptions, url);
// LOGGING: options.log can be set to `null` to defeat logging
if (mergedOptions.log === null) {
mergedOptions.log = new NullLog();
}
mergeNestedFields(mergedOptions, getGlobalLoaderOptions());
mergeNestedFields(mergedOptions, options);
return mergedOptions;
}
// Merge nested options objects
function mergeNestedFields(mergedOptions: LoaderOptions, options: LoaderOptions): void {
for (const key in options) {
// Check for nested options
// object in options => either no key in defaultOptions or object in defaultOptions
if (key in options) {
const value = options[key];
if (isPureObject(value) && isPureObject(mergedOptions[key])) {
mergedOptions[key] = {
...(mergedOptions[key] as object),
...(options[key] as object)
};
} else {
mergedOptions[key] = options[key];
}
}
// else: No need to merge nested opts, and the initial merge already copied over the nested options
}
}
/**
* Harvest information from the url
* @deprecated This is mainly there to support a hack in the GLTFLoader
* TODO - baseUri should be a directory, i.e. remove file component from baseUri
* TODO - extract extension?
* TODO - extract query parameters?
* TODO - should these be injected on context instead of options?
*/
function addUrlOptions(options: LoaderOptions, url?: string): void {
if (url && !('baseUri' in options)) {
options.baseUri = url;
}
}