diff --git a/src/mol-plugin-state/actions/volume.ts b/src/mol-plugin-state/actions/volume.ts index 8827bb8795b31f2d8b24e66774d246c8d140b474..951d5a5202382b86fae047a37cace412980edcd1 100644 --- a/src/mol-plugin-state/actions/volume.ts +++ b/src/mol-plugin-state/actions/volume.ts @@ -114,7 +114,7 @@ const DownloadDensity = StateAction.build({ switch (src.name) { case 'url': downloadParams = src.params; - provider = src.params.format === 'auto' ? plugin.dataFormats.auto(getFileInfo(downloadParams.url.url), data.cell?.obj!) : plugin.dataFormats.get(src.params.format); + provider = src.params.format === 'auto' ? plugin.dataFormats.auto(getFileInfo(Asset.getUrl(downloadParams.url)), data.cell?.obj!) : plugin.dataFormats.get(src.params.format); break; case 'pdb-xray': provider = src.params.provider.server === 'pdbe' diff --git a/src/mol-plugin-state/transforms/data.ts b/src/mol-plugin-state/transforms/data.ts index 9beac175616bb5f5572412e6574cd26518ebe958..505324180c2c9d7f99be7e7c916ab5c7b71a8d8f 100644 --- a/src/mol-plugin-state/transforms/data.ts +++ b/src/mol-plugin-state/transforms/data.ts @@ -47,11 +47,12 @@ const Download = PluginStateTransform.BuiltIn({ })({ apply({ params: p, cache }, plugin: PluginContext) { return Task.create('Download', async ctx => { - const asset = await plugin.managers.asset.resolve(p.url, p.isBinary ? 'binary' : 'string').runInContext(ctx); + let url = Asset.getUrlAsset(p.url, plugin.managers.asset); + const asset = await plugin.managers.asset.resolve(url, p.isBinary ? 'binary' : 'string').runInContext(ctx); (cache as any).asset = asset; return p.isBinary - ? new SO.Data.Binary(asset.data as Uint8Array, { label: p.label ? p.label : p.url.url }) - : new SO.Data.String(asset.data as string, { label: p.label ? p.label : p.url.url }); + ? new SO.Data.Binary(asset.data as Uint8Array, { label: p.label ? p.label : url.url }) + : new SO.Data.String(asset.data as string, { label: p.label ? p.label : url.url }); }); }, dispose({ cache }) { @@ -60,7 +61,7 @@ const Download = PluginStateTransform.BuiltIn({ update({ oldParams, newParams, b }) { if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return StateTransformer.UpdateResult.Recreate; if (oldParams.label !== newParams.label) { - b.label = newParams.label || newParams.url.url; + b.label = newParams.label || ((typeof newParams.url === 'string') ? newParams.url : newParams.url.url); return StateTransformer.UpdateResult.Updated; } return StateTransformer.UpdateResult.Unchanged; diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index c8c0a879e2a52665673c42d03892351b1ec62596..f9488d728e4ddd325d1487678afb08e846254899 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/src/mol-plugin-ui/controls/parameters.tsx @@ -790,7 +790,7 @@ export class Mat4Control extends React.PureComponent<ParamProps<PD.Mat4>, { isEx export class UrlControl extends SimpleParam<PD.UrlParam> { onChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; - if (value !== this.props.value.url) { + if (value !== Asset.getUrl(this.props.value || '')) { this.update(Asset.Url(value)); } } @@ -805,7 +805,7 @@ export class UrlControl extends SimpleParam<PD.UrlParam> { renderControl() { const placeholder = this.props.param.label || camelCaseToWords(this.props.name); return <input type='text' - value={this.props.value.url || ''} + value={Asset.getUrl(this.props.value || '')} placeholder={placeholder} onChange={this.onChange} onKeyPress={this.props.onEnter ? this.onKeyPress : void 0} diff --git a/src/mol-util/assets.ts b/src/mol-util/assets.ts index ba06cfec45d1ef2b255e45fd822647522705d4de..635d7b10c75906c0686a3fb67d2c0c016e1f8830 100644 --- a/src/mol-util/assets.ts +++ b/src/mol-util/assets.ts @@ -44,27 +44,51 @@ namespace Asset { } } + + export function getUrl(url: string | Url) { + return typeof url === 'string' ? url : url.url; + } + + export function getUrlAsset(url: string | Url, manager: AssetManager) { + if (typeof url === 'string') { + const asset = manager.tryFindUrl(url); + return asset || Url(url); + } + return url; + } } class AssetManager { // TODO: add URL based ref-counted cache? // TODO: when serializing, check for duplicates? - private _assets = new Map<string, { asset: Asset, file: File }>(); + private _assets = new Map<string, { asset: Asset, file: File, refCount: number }>(); get assets() { return iterableToArray(this._assets.values()); } + tryFindUrl(url: string, body?: string): Asset.Url | undefined { + const assets = this.assets.values(); + while (true) { + const v = assets.next(); + if (v.done) return; + const asset = v.value.asset; + if (Asset.isUrl(asset) && asset.url === url && (asset.body || '') === (body || '')) return asset; + } + } + set(asset: Asset, file: File) { - this._assets.set(asset.id, { asset, file }); + this._assets.set(asset.id, { asset, file, refCount: 0 }); } resolve<T extends DataType>(asset: Asset, type: T, store = true): Task<Asset.Wrapper<T>> { if (Asset.isUrl(asset)) { return Task.create(`Download ${asset.title || asset.url}`, async ctx => { if (this._assets.has(asset.id)) { - return new Asset.Wrapper(await readFromFile(this._assets.get(asset.id)!.file, type).runInContext(ctx), asset, this); + const entry = this._assets.get(asset.id)!; + entry.refCount++; + return new Asset.Wrapper(await readFromFile(entry.file, type).runInContext(ctx), asset, this); } if (!store) { @@ -73,19 +97,21 @@ class AssetManager { const data = await ajaxGet({ ...asset, type: 'binary' }).runInContext(ctx); const file = new File([data], 'raw-data'); - this._assets.set(asset.id, { asset, file }); + this._assets.set(asset.id, { asset, file, refCount: 1 }); return new Asset.Wrapper(await readFromFile(file, type).runInContext(ctx), asset, this); }); } else { return Task.create(`Read ${asset.name}`, async ctx => { if (this._assets.has(asset.id)) { - return new Asset.Wrapper(await readFromFile(this._assets.get(asset.id)!.file, type).runInContext(ctx), asset, this); + const entry = this._assets.get(asset.id)!; + entry.refCount++; + return new Asset.Wrapper(await readFromFile(entry.file, type).runInContext(ctx), asset, this); } if (!(asset.file instanceof File)) { throw new Error(`Cannot resolve file asset '${asset.name}' (${asset.id})`); } if (store) { - this._assets.set(asset.id, { asset, file: asset.file }); + this._assets.set(asset.id, { asset, file: asset.file, refCount: 1 }); } return new Asset.Wrapper(await readFromFile(asset.file, type).runInContext(ctx), asset, this); }); @@ -93,6 +119,9 @@ class AssetManager { } release(asset: Asset) { - this._assets.delete(asset.id); + const entry = this._assets.get(asset.id); + if (!entry) return; + entry.refCount--; + if (entry.refCount <= 0) this._assets.delete(asset.id); } } \ No newline at end of file diff --git a/src/mol-util/data-source.ts b/src/mol-util/data-source.ts index 49d44d27d5c866dc1c4fed63de6ef448521b0844..c421e85ec363517416f0836f7b3528144960928a 100644 --- a/src/mol-util/data-source.ts +++ b/src/mol-util/data-source.ts @@ -282,7 +282,7 @@ function ajaxGetInternal<T extends DataType>(title: string | undefined, url: str } export type AjaxGetManyEntry = { kind: 'ok', id: string, result: Asset.Wrapper<'string' | 'binary'> } | { kind: 'error', id: string, error: any } -export async function ajaxGetMany(ctx: RuntimeContext, assetManager: AssetManager, sources: { id: string, url: Asset.Url, isBinary?: boolean, canFail?: boolean }[], maxConcurrency: number) { +export async function ajaxGetMany(ctx: RuntimeContext, assetManager: AssetManager, sources: { id: string, url: Asset.Url | string, isBinary?: boolean, canFail?: boolean }[], maxConcurrency: number) { const len = sources.length; const slots: AjaxGetManyEntry[] = new Array(sources.length); @@ -293,7 +293,7 @@ export async function ajaxGetMany(ctx: RuntimeContext, assetManager: AssetManage const current = sources[currentSrc]; promises.push(wrapPromise(currentSrc, current.id, - assetManager.resolve(current.url, current.isBinary ? 'binary' : 'string').runAsChild(ctx))); + assetManager.resolve(Asset.getUrlAsset(current.url, assetManager), current.isBinary ? 'binary' : 'string').runAsChild(ctx))); promiseKeys.push(currentSrc); } @@ -315,7 +315,7 @@ export async function ajaxGetMany(ctx: RuntimeContext, assetManager: AssetManage promiseKeys = promiseKeys.filter(_filterRemoveIndex, idx); if (currentSrc < len) { const current = sources[currentSrc]; - const asset = assetManager.resolve(current.url, current.isBinary ? 'binary' : 'string').runAsChild(ctx); + const asset = assetManager.resolve(Asset.getUrlAsset(current.url, assetManager), current.isBinary ? 'binary' : 'string').runAsChild(ctx); promises.push(wrapPromise(currentSrc, current.id, asset)); promiseKeys.push(currentSrc); currentSrc++; diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index b6c53baa0ba57faf64489c3535fb279ef79e8b11..db425f90f163fcee86dd73b0064bf2c839633c34 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -148,7 +148,7 @@ export namespace ParamDefinition { return setInfo<Mat4>({ type: 'mat4', defaultValue }, info); } - export interface UrlParam extends Base<Asset.Url> { + export interface UrlParam extends Base<Asset.Url | string> { type: 'url' } export function Url(url: string | { url: string, body?: string }, info?: Info): UrlParam {