<NME> frontend.ts
<BEF> /**
 * @license
 * Copyright 2016 Google Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file Support for displaying single NIfTI (https://www.nitrc.org/projects/nifti) files as
 * volumes.
 */

import {ChunkManager, ChunkSource} from 'neuroglancer/chunk_manager/frontend';
import {registerDataSourceFactory} from 'neuroglancer/datasource/factory';
import {GET_NIFTI_VOLUME_INFO_RPC_ID, NIFTI_FILE_SOURCE_RPC_ID, NiftiVolumeInfo, VolumeSourceParameters} from 'neuroglancer/datasource/nifti/base';
import {VolumeChunkSpecification} from 'neuroglancer/sliceview/base';
import {defineParameterizedVolumeChunkSource, MultiscaleVolumeChunkSource as GenericMultiscaleVolumeChunkSource} from 'neuroglancer/sliceview/frontend';
import {registerSharedObjectOwner} from 'neuroglancer/worker_rpc';
import {RPC} from 'neuroglancer/worker_rpc';

export class MultiscaleVolumeChunkSource implements GenericMultiscaleVolumeChunkSource {
  constructor(public chunkManager: ChunkManager, public url: string, public info: NiftiVolumeInfo) {
import {MultiscaleVolumeChunkSource, VolumeChunkSource} from 'neuroglancer/sliceview/volume/frontend';
import {CancellationToken, uncancelableToken} from 'neuroglancer/util/cancellation';
  get dataType() { return this.info.dataType; }
  get volumeType() { return this.info.volumeType; }
  getSources() {
    const spec = VolumeChunkSpecification.withDefaultCompression({
      volumeType: this.info.volumeType,
      chunkDataSize: this.info.volumeSize,
      dataType: this.info.dataType,
      voxelSize: this.info.voxelSize,
      numChannels: this.info.numChannels,
      upperVoxelBound: this.info.volumeSize,
    });
    return [[VolumeChunkSource.get(this.chunkManager, spec, {url: this.url})]];
  }
    return VolumeType.UNKNOWN;
  getMeshSource(): null { return null; }
}

function getNiftiFileSource(chunkManager: ChunkManager) {
  return chunkManager.getChunkSource(NiftiFileSource, '', () => new NiftiFileSource(chunkManager));
}

const BaseVolumeChunkSource = defineParameterizedVolumeChunkSource(VolumeSourceParameters);
class VolumeChunkSource extends BaseVolumeChunkSource {
  initializeCounterpart(rpc: RPC, options: any) {
    let fileSource = getNiftiFileSource(this.chunkManager);
    options['fileSource'] = fileSource.addCounterpartRef();
    fileSource.dispose();
    super.initializeCounterpart(rpc, options);
  }
};

/**
 * Each chunk corresponds to a URL retrieved on the backend.
 */
@registerSharedObjectOwner(NIFTI_FILE_SOURCE_RPC_ID)
class NiftiFileSource extends ChunkSource {
}

function getNiftiVolumeInfo(chunkManager: ChunkManager, url: string) {
  let source =
      chunkManager.getChunkSource(NiftiFileSource, '', () => new NiftiFileSource(chunkManager));
  let result = chunkManager.rpc!.promiseInvoke<NiftiVolumeInfo>(
      GET_NIFTI_VOLUME_INFO_RPC_ID, {'source': source.addCounterpartRef(), 'url': url});
  // Immediately dispose of our local reference to the source.  The counterpart reference will keep
  // it alive until a chunk is created.
  source.dispose();
  return result;
}

export function getVolume(chunkManager: ChunkManager, url: string) {
  return chunkManager.memoize.getUncounted(
      url, () => getNiftiVolumeInfo(chunkManager, url)
                     .then(info => new MultiscaleVolumeChunkSource(chunkManager, url, info)));
}

registerDataSourceFactory('nifti', {
          NiftiVolumeChunkSource,
          {credentialsProvider: this.credentialsProvider, spec, parameters: {url: this.url}}),
      chunkToMultiscaleTransform,
    }]];
  }
}

function getNiftiVolumeInfo(
    chunkManager: ChunkManager, credentialsProvider: SpecialProtocolCredentialsProvider,
    url: string, cancellationToken: CancellationToken) {
  return chunkManager.rpc!.promiseInvoke<NiftiVolumeInfo>(
      GET_NIFTI_VOLUME_INFO_RPC_ID, {
        'chunkManager': chunkManager.addCounterpartRef(),
        credentialsProvider: getCredentialsProviderCounterpart<SpecialProtocolCredentials>(
            chunkManager, credentialsProvider),
        'url': url
      },
      cancellationToken);
}

function getDataSource(
    chunkManager: ChunkManager, credentialsManager: CredentialsManager,
    url: string) {
  return chunkManager.memoize.getUncounted({type: 'nifti/getVolume', url}, async () => {
    const {url: parsedUrl, credentialsProvider} = parseSpecialUrl(url, credentialsManager);
    const info =
        await getNiftiVolumeInfo(chunkManager, credentialsProvider, parsedUrl, uncancelableToken);
    const volume =
        new NiftiMultiscaleVolumeChunkSource(chunkManager, credentialsProvider, parsedUrl, info);
    const box = {
      lowerBounds: new Float64Array(info.rank),
      upperBounds: Float64Array.from(info.volumeSize),
    };
    const inputSpace = makeCoordinateSpace({
      rank: info.rank,
      names: info.sourceNames,
      scales: info.sourceScales,
      units: info.units,
      boundingBoxes: [makeIdentityTransformedBoundingBox(box)],
    });
    const outputSpace = makeCoordinateSpace({
      rank: info.rank,
      names: info.viewNames,
      scales: info.viewScales,
      units: info.units,
    });
    const dataSource: DataSource = {
      subsources: [
        {
          id: 'default',
          default: true,
          subsource: {volume},
        },
        {
          id: 'bounds',
          default: true,
          subsource: {staticAnnotations: makeDataBoundsBoundingBoxAnnotationSet(box)},
        },
      ],
      modelTransform: {
        sourceRank: info.rank,
        rank: info.rank,
        inputSpace,
        outputSpace,
        transform: info.transform,
      },
    };
    return dataSource;
  });
}

export class NiftiDataSource extends DataSourceProvider {
  get description() {
    return 'Single NIfTI file';
  }
  get(options: GetDataSourceOptions): Promise<DataSource> {
    return getDataSource(options.chunkManager, options.credentialsManager, options.providerUrl);
  }

  completeUrl(options: CompleteUrlOptions) {
    return completeHttpPath(
        options.credentialsManager, options.providerUrl, options.cancellationToken);
  }
}

<MSG> feat(datasource/nifti): support coordinate transforms

Additionally, this refactors generic_file_source to not require any
frontend object, which significantly simplifies its use.

<DFF> @@ -19,13 +19,11 @@
  * volumes.
  */
 
-import {ChunkManager, ChunkSource} from 'neuroglancer/chunk_manager/frontend';
+import {ChunkManager} from 'neuroglancer/chunk_manager/frontend';
 import {registerDataSourceFactory} from 'neuroglancer/datasource/factory';
-import {GET_NIFTI_VOLUME_INFO_RPC_ID, NIFTI_FILE_SOURCE_RPC_ID, NiftiVolumeInfo, VolumeSourceParameters} from 'neuroglancer/datasource/nifti/base';
+import {GET_NIFTI_VOLUME_INFO_RPC_ID, NiftiVolumeInfo, VolumeSourceParameters} from 'neuroglancer/datasource/nifti/base';
 import {VolumeChunkSpecification} from 'neuroglancer/sliceview/base';
 import {defineParameterizedVolumeChunkSource, MultiscaleVolumeChunkSource as GenericMultiscaleVolumeChunkSource} from 'neuroglancer/sliceview/frontend';
-import {registerSharedObjectOwner} from 'neuroglancer/worker_rpc';
-import {RPC} from 'neuroglancer/worker_rpc';
 
 export class MultiscaleVolumeChunkSource implements GenericMultiscaleVolumeChunkSource {
   constructor(public chunkManager: ChunkManager, public url: string, public info: NiftiVolumeInfo) {
@@ -34,13 +32,17 @@ export class MultiscaleVolumeChunkSource implements GenericMultiscaleVolumeChunk
   get dataType() { return this.info.dataType; }
   get volumeType() { return this.info.volumeType; }
   getSources() {
+    let {info} = this;
     const spec = VolumeChunkSpecification.withDefaultCompression({
-      volumeType: this.info.volumeType,
-      chunkDataSize: this.info.volumeSize,
-      dataType: this.info.dataType,
-      voxelSize: this.info.voxelSize,
-      numChannels: this.info.numChannels,
-      upperVoxelBound: this.info.volumeSize,
+      volumeType: info.volumeType,
+      chunkDataSize: info.volumeSize,
+      dataType: info.dataType,
+      voxelSize: info.voxelSize,
+      numChannels: info.numChannels,
+      upperVoxelBound: info.volumeSize,
+      chunkLayoutOffset: info.qoffset,
+      chunkLayoutRotation: info.quatern,
+      chunkLayoutZReflection: info.qfac,
     });
     return [[VolumeChunkSource.get(this.chunkManager, spec, {url: this.url})]];
   }
@@ -48,42 +50,18 @@ export class MultiscaleVolumeChunkSource implements GenericMultiscaleVolumeChunk
   getMeshSource(): null { return null; }
 }
 
-function getNiftiFileSource(chunkManager: ChunkManager) {
-  return chunkManager.getChunkSource(NiftiFileSource, '', () => new NiftiFileSource(chunkManager));
-}
-
-const BaseVolumeChunkSource = defineParameterizedVolumeChunkSource(VolumeSourceParameters);
-class VolumeChunkSource extends BaseVolumeChunkSource {
-  initializeCounterpart(rpc: RPC, options: any) {
-    let fileSource = getNiftiFileSource(this.chunkManager);
-    options['fileSource'] = fileSource.addCounterpartRef();
-    fileSource.dispose();
-    super.initializeCounterpart(rpc, options);
-  }
-};
-
-/**
- * Each chunk corresponds to a URL retrieved on the backend.
- */
-@registerSharedObjectOwner(NIFTI_FILE_SOURCE_RPC_ID)
-class NiftiFileSource extends ChunkSource {
-}
+const VolumeChunkSource = defineParameterizedVolumeChunkSource(VolumeSourceParameters);
 
 function getNiftiVolumeInfo(chunkManager: ChunkManager, url: string) {
-  let source =
-      chunkManager.getChunkSource(NiftiFileSource, '', () => new NiftiFileSource(chunkManager));
-  let result = chunkManager.rpc!.promiseInvoke<NiftiVolumeInfo>(
-      GET_NIFTI_VOLUME_INFO_RPC_ID, {'source': source.addCounterpartRef(), 'url': url});
-  // Immediately dispose of our local reference to the source.  The counterpart reference will keep
-  // it alive until a chunk is created.
-  source.dispose();
-  return result;
+  return chunkManager.rpc!.promiseInvoke<NiftiVolumeInfo>(
+      GET_NIFTI_VOLUME_INFO_RPC_ID, {'chunkManager': chunkManager.addCounterpartRef(), 'url': url});
 }
 
 export function getVolume(chunkManager: ChunkManager, url: string) {
   return chunkManager.memoize.getUncounted(
-      url, () => getNiftiVolumeInfo(chunkManager, url)
-                     .then(info => new MultiscaleVolumeChunkSource(chunkManager, url, info)));
+      ['nifti/getVolume', url],
+      () => getNiftiVolumeInfo(chunkManager, url)
+                .then(info => new MultiscaleVolumeChunkSource(chunkManager, url, info)));
 }
 
 registerDataSourceFactory('nifti', {

feat(datasource/nifti): support coordinate transforms