UNPKG

@loaders.gl/core

Version:

The core API for working with loaders.gl loaders and writers

215 lines (191 loc) 7.15 kB
// 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; } }