diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..b512c09d476623ff4bf8d0d63c29b784925dbdf8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3c07d114df83ae2c2dc663da720cc5451914f31..0ee38faacdd75f02e4d4c8567b72065c7d9ef719 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 14.x for lint + - name: Use Node.js 16.x for lint uses: actions/setup-node@v1 with: - node-version: '14.x' + node-version: '16.x' - run: npm i - run: npm run lint @@ -26,38 +26,37 @@ jobs: if: always() runs-on: ubuntu-latest - strategy: - matrix: - node-version: [12.x, 14.x, 16.x] - env: NODE_ENV: test steps: - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js 16.x uses: actions/setup-node@v1 with: - node-version: ${{ matrix.node-version }} + node-version: 16.x - run: npm i - - run: npm run test-ci + - run: | + if [[ "$GITHUB_REF" = *hotfix* ]] || [[ "$GITHUB_REF" = refs/heads/staging ]] + then + export SIIBRA_API_ENDPOINTS=https://siibra-api-rc.apps.hbp.eu/v2_0,https://siibra-api-rc.apps.jsc.hbp.eu/v2_0 + node src/environments/parseEnv.js ./environment.ts + fi + npm run test-ci backend: if: always() runs-on: ubuntu-latest - strategy: - matrix: - node-version: [12.x, 14.x, 16.x] env: NODE_ENV: test steps: - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js 16.x uses: actions/setup-node@v1 with: - node-version: ${{ matrix.node-version }} + node-version: 16.x - run: | cd deploy npm i diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index 47734fc27ba6540ee111646142553af53a365798..057a93d7864f37820b14b5cf8f6ce141a6a08f85 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -18,7 +18,7 @@ jobs: PRODUCTION: 'true' DOCKER_REGISTRY: 'docker-registry.ebrains.eu/siibra/' - SIIBRA_API_STABLE: 'https://siibra-api-stable.apps.hbp.eu/v2_0' + SIIBRA_API_STABLE: 'https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0' SIIBRA_API_RC: 'https://siibra-api-rc.apps.hbp.eu/v2_0' SIIBRA_API_LATEST: 'https://siibra-api-latest.apps-dev.hbp.eu/v2_0' @@ -37,31 +37,24 @@ jobs: if [[ "$GITHUB_REF" == 'refs/heads/master' ]] then echo "Either master, using prod env..." - echo "BS_REST_URL=${{ env.SIIBRA_API_STABLE }}" >> $GITHUB_ENV + echo "SIIBRA_API_ENDPOINTS=${{ env.SIIBRA_API_STABLE }}" >> $GITHUB_ENV elif [[ "$GITHUB_REF" == 'refs/heads/staging' ]] then echo "Either staging, using staging env..." - echo "BS_REST_URL=${{ env.SIIBRA_API_RC }}" >> $GITHUB_ENV + echo "SIIBRA_API_ENDPOINTS=${{ env.SIIBRA_API_RC }}" >> $GITHUB_ENV else if [[ "$GITHUB_REF" == *hotfix* ]] then echo "Hotfix branch, using prod env..." - echo "BS_REST_URL=${{ env.SIIBRA_API_RC }}" >> $GITHUB_ENV + echo "SIIBRA_API_ENDPOINTS=${{ env.SIIBRA_API_RC }}" >> $GITHUB_ENV else echo "Using dev env..." - echo "BS_REST_URL=${{ env.SIIBRA_API_LATEST }}" >> $GITHUB_ENV + echo "SIIBRA_API_ENDPOINTS=${{ env.SIIBRA_API_LATEST }}" >> $GITHUB_ENV fi fi - name: 'Set version variable & expmt feature flag' run: | - GIT_HASH=$(git rev-parse --short HEAD) - echo "Setting GIT_HASH: $GIT_HASH" - echo "GIT_HASH=$GIT_HASH" >> $GITHUB_ENV - - VERSION=$(jq -r '.version' package.json) - echo "Setting VERSION: $VERSION" - echo "VERSION=$VERSION" >> $GITHUB_ENV if [[ "$GITHUB_REF" == 'refs/heads/master' ]] || [[ "$GITHUB_REF" == 'refs/heads/staging' ]] then echo "prod/staging build, do not enable experimental features" @@ -74,11 +67,9 @@ jobs: DOCKER_BUILT_TAG=${{ env.DOCKER_REGISTRY }}siibra-explorer:$BRANCH_NAME echo "Building $DOCKER_BUILT_TAG" docker build \ - --build-arg GIT_HASH=$GIT_HASH \ - --build-arg VERSION=$VERSION \ --build-arg MATOMO_URL=$MATOMO_URL \ --build-arg MATOMO_ID=$MATOMO_ID \ - --build-arg BS_REST_URL=$BS_REST_URL \ + --build-arg SIIBRA_API_ENDPOINTS=$SIIBRA_API_ENDPOINTS \ --build-arg EXPERIMENTAL_FEATURE_FLAG=$EXPERIMENTAL_FEATURE_FLAG \ -t $DOCKER_BUILT_TAG \ . diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 7263cbb65b77246cb748b3c7f1358d119578aea4..f63b14cd95658b20dc7795ad8804b6e322020b50 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -11,3 +11,4 @@ </style> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"> <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" defer></script> +<link rel="stylesheet" href="icons/iav-icons.css"> diff --git a/.storybook/preview.js b/.storybook/preview.js index 957e7cbef107fa2c3191e57245241fb901983f17..52750e8cde9405a8fc3282ed73730bc13101605d 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -4,6 +4,14 @@ setCompodocJson(docJson); import 'src/theme.scss' +/** + * load custom icons + */ +import '!!file-loader?context=src/res&name=icons/iav-icons.css!src/res/icons/iav-icons.css' +import '!!file-loader?context=src/res&name=icons/iav-icons.ttf!src/res/icons/iav-icons.ttf' +import '!!file-loader?context=src/res&name=icons/iav-icons.woff!src/res/icons/iav-icons.woff' +import '!!file-loader?context=src/res&name=icons/iav-icons.svg!src/res/icons/iav-icons.svg' + export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { diff --git a/Dockerfile b/Dockerfile index 17e735232f80890da6b5b2b65db1b33c152c9765..8425647048ad716c0b86edb66ad609b36aa01a84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ FROM node:14 as builder ARG BACKEND_URL ENV BACKEND_URL=${BACKEND_URL} -ARG BS_REST_URL -ENV BS_REST_URL=${BS_REST_URL:-https://siibra-api-stable.apps.hbp.eu/v1_0} +ARG SIIBRA_API_ENDPOINTS +ENV SIIBRA_API_ENDPOINTS=${SIIBRA_API_ENDPOINTS:-https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0} ARG STRICT_LOCAL ENV STRICT_LOCAL=${STRICT_LOCAL:-false} @@ -21,11 +21,9 @@ ENV MATOMO_ID=${MATOMO_ID} ARG EXPERIMENTAL_FEATURE_FLAG ENV EXPERIMENTAL_FEATURE_FLAG=${EXPERIMENTAL_FEATURE_FLAG:-false} -ARG GIT_HASH -ENV GIT_HASH=${GIT_HASH:-unknownhash} +ARG ENABLE_LEAP_MOTION +ENV ENABLE_LEAP_MOTION=${ENABLE_LEAP_MOTION:-false} -ARG VERSION -ENV VERSION=${VERSION:-unknownversion} COPY . /iv WORKDIR /iv @@ -39,6 +37,7 @@ RUN rm -rf ./node_modules RUN npm i RUN npm run build +RUN node third_party/matomo/processMatomo.js RUN npm run build-storybook # gzipping container diff --git a/angular.json b/angular.json index 9797b5413cb83b570a3695fe1a1bdca44b6a2de7..48fe4b228e4f2fca1ed066681e73248be78ab271 100644 --- a/angular.json +++ b/angular.json @@ -80,6 +80,10 @@ "input": "export-nehuba/dist/min/chunk_worker.bundle.js", "inject": false, "bundleName": "chunk_worker.bundle" + },{ + "inject": false, + "input": "third_party/leap-0.6.4.js", + "bundleName": "leap-0.6.4" }] }, "configurations": { diff --git a/build_env.md b/build_env.md index 1c29ea98614c3a40dbe91d7c620f9e6a5356b4e8..81833e992e2dd7b5e484e0593cee4409e128c603 100644 --- a/build_env.md +++ b/build_env.md @@ -4,12 +4,13 @@ As interactive atlas viewer uses [webpack define plugin](https://webpack.js.org/ | name | description | default | example | | --- | --- | --- | --- | -| `VERSION` | printed in console on viewer startup | `GIT_HASH` \|\| unspecificed hash | v2.2.2 | | `PRODUCTION` | if the build is for production, toggles optimisations such as minification | `undefined` | true | | `BACKEND_URL` | backend that the viewer calls to fetch available template spaces, parcellations, plugins, datasets | `null` | https://interactive-viewer.apps.hbp.eu/ | -| `BS_REST_URL` | [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | https://siibra-api-stable.apps.hbp.eu/v1_0 | +| ~~`BS_REST_URL`~~ _deprecated. use `SIIBRA_API_ENDPOINTS` instead_ | [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | `https://siibra-api-stable.apps.hbp.eu/v1_0` | +| `SIIBRA_API_ENDPOINTS` | Comma separated endpoints of [siibra-api](https://github.com/FZJ-INM1-BDA/siibra-api) used to fetch different resources | `https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0` | | `MATOMO_URL` | base url for matomo analytics | `null` | https://example.com/matomo/ | | `MATOMO_ID` | application id for matomo analytics | `null` | 6 | | `STRICT_LOCAL` | hides **Explore** and **Download** buttons. Useful for offline demo's | `false` | `true` | | `KIOSK_MODE` | after 5 minutes of inactivity, shows overlay inviting users to interact | `false` | `true` | -| `BUILD_TEXT` | overlay text at bottom right of the viewer. set to `''` to hide. | | +| `EXPERIMENTAL_FEATURE_FLAG` | enabling experimental features | `false` | `true` | +| `ENABLE_LEAP_MOTION` | enable leap motion controller | `false` | `true` | diff --git a/common/constants.js b/common/constants.js index cf56106555a3b26559711671e22fcc0e06494e86..c1fe2db5b6981123c9811587a0523a859b95c234 100644 --- a/common/constants.js +++ b/common/constants.js @@ -108,6 +108,7 @@ If you do not accept the Terms & Conditions you are not permitted to access or u CANNOT_DECIPHER_HEMISPHERE: 'Cannot decipher region hemisphere.', DOES_NOT_SUPPORT_MULTI_REGION_SELECTION: `Please only select a single region.`, MULTI_REGION_SELECTION: `Multi region selection`, + DESCRIPTION: 'Description', REGIONAL_FEATURES: 'Regional features', CONNECTIVITY: 'Connectivity', NO_ADDIONTAL_INFO_AVAIL: `Currently, no additional information is linked to this region.`, @@ -135,7 +136,11 @@ If you do not accept the Terms & Conditions you are not permitted to access or u LOADING_ANNOTATION_MSG: `Loading annotations... Please wait...`, ATLAS_SELECTOR_LABEL_SPACES: `Spaces`, - ATLAS_SELECTOR_LABEL_PARC_MAPS: `Parcellation maps` + ATLAS_SELECTOR_LABEL_PARC_MAPS: `Parcellation maps`, + + TOGGLE_LAYER_VISILITY: 'Toggle layer visility', + ORIENT_TO_LAYER: 'Orient to layer native orientation', + CONFIGURE_LAYER: 'Configure layer' } exports.QUICKTOUR_DESC ={ diff --git a/common/helpOnePager.md b/common/helpOnePager.md index a8551e0b2b22258b633459a5854774ac301f8ae0..4fb8f16ec04794c71a642a1d92526a6a2e53800c 100644 --- a/common/helpOnePager.md +++ b/common/helpOnePager.md @@ -10,6 +10,7 @@ | Next slice | `<ctrl>` + `[mousewheel]` | - | | Next 10 slice | `<ctrl>` + `<shift>` + `[mousewheel]` | - | | Toggle delineation | `[q]` | - | +| Toggle cross hair | `[a]` | - | --- diff --git a/common/util.js b/common/util.js index 198b3f0b6a0de651198849214120e918799e8af9..44f458cafc4884e64f1a225c6d11a3425fbbda68 100644 --- a/common/util.js +++ b/common/util.js @@ -239,5 +239,78 @@ exports.isVec4 = isVec4 exports.isMat4 = isMat4 + const cipher = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-' + const negString = '~' + + const encodeInt = (number) => { + if (number % 1 !== 0) { throw new Error('cannot encodeInt on a float. Ensure float flag is set') } + if (isNaN(Number(number)) || number === null || number === Number.POSITIVE_INFINITY) { throw new Error('The input is not valid') } + + let residual + let result = '' + + if (number < 0) { + result += negString + residual = Math.floor(number * -1) + } else { + residual = Math.floor(number) + } + + /* eslint-disable-next-line no-constant-condition */ + while (true) { + result = cipher.charAt(residual % 64) + result + residual = Math.floor(residual / 64) + + if (residual === 0) { + break + } + } + return result + } + const decodeToInt = (encodedString) => { + let _encodedString + let negFlag = false + if (encodedString.slice(-1) === negString) { + negFlag = true + _encodedString = encodedString.slice(0, -1) + } else { + _encodedString = encodedString + } + return (negFlag ? -1 : 1) * [..._encodedString].reduce((acc, curr) => { + const index = cipher.indexOf(curr) + if (index < 0) { throw new Error(`Poisoned b64 encoding ${encodedString}`) } + return acc * 64 + index + }, 0) + } + + exports.sxplrNumB64Enc = { + separator: ".", + cipher, + encodeNumber: (number, opts = { float: false }) => { + const { float } = opts + if (!float) { + return encodeInt(number) + } else { + const floatArray = new Float32Array(1) + floatArray[0] = number + const intArray = new Uint32Array(floatArray.buffer) + const castedInt = intArray[0] + return encodeInt(castedInt) + } + }, + decodeToNumber: (encodedString, opts = { float: false }) => { + const { float } = opts + if (!float) { + return decodeToInt(encodedString) + } else { + const _int = decodeToInt(encodedString) + const intArray = new Uint32Array(1) + intArray[0] = _int + const castedFloat = new Float32Array(intArray.buffer) + return castedFloat[0] + } + } + } + })(typeof exports === 'undefined' ? module.exports : exports) diff --git a/deploy/app.js b/deploy/app.js index 7b48f151dcd9e9e9fc8d9595b37eab27d00e02b4..f4b33a1c39d3904ace5651ca22ed2a909e8d269b 100644 --- a/deploy/app.js +++ b/deploy/app.js @@ -7,7 +7,7 @@ const crypto = require('crypto') const cookieParser = require('cookie-parser') const bkwdMdl = require('./bkwdCompat')() -const LOCAL_CDN_FLAG = !!process.env.PRECOMPUTED_SERVER +const LOCAL_CDN_FLAG = !!process.env.LOCAL_CDN if (process.env.NODE_ENV !== 'production') { app.use(require('cors')()) @@ -126,7 +126,8 @@ if (LOCAL_CDN_FLAG) { const LOCAL_CDN = process.env.LOCAL_CDN const CDN_ARRAY = [ 'https://stackpath.bootstrapcdn.com', - 'https://use.fontawesome.com' + 'https://use.fontawesome.com', + 'https://unpkg.com' ] let indexFile @@ -214,8 +215,7 @@ app.get('/ready', async (req, res) => { * only use compression for production * this allows locally built aot to be served without errors */ -const { compressionMiddleware, setAlwaysOff } = require('nomiseco') -if (LOCAL_CDN_FLAG) setAlwaysOff(true) +const { compressionMiddleware } = require('nomiseco') app.use(compressionMiddleware, express.static(PUBLIC_PATH)) diff --git a/deploy/bkwdCompat/urlState.js b/deploy/bkwdCompat/urlState.js index 66f92c3dc4f6b375fcfe0c70f16a685b43c30e0d..52b245bcc54d7b6c1bc2a479a354651e215143e7 100644 --- a/deploy/bkwdCompat/urlState.js +++ b/deploy/bkwdCompat/urlState.js @@ -1,6 +1,12 @@ // this module is suppose to rewrite state stored in query param // and convert it to path based url -const separator = '.' +const { sxplrNumB64Enc } = require("../../common/util") + +const { + encodeNumber, + separator +} = sxplrNumB64Enc + const waxolmObj = { aId: 'minds/core/parcellationatlas/v1.0.0/522b368e-49a3-49fa-88d3-0870a307974a', id: 'minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8', @@ -114,8 +120,12 @@ const WARNING_STRINGS = { REGION_SELECT_ERROR: 'Region selected cannot be processed properly.', TEMPLATE_ERROR: 'Template not found.', } - +const pliPreviewUrl = `/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:a1655b99-82f1-420f-a3c2-fe80fd4c8588/p:juelich:iav:atlas:v1.0.0:4/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIy..1qI1a.D31U~.i-Os~..HRE/f:siibra:features:voi:19c437087299dd62e7c507200f69aea6` module.exports = (query, _warningCb) => { + + const HOST_PATHNAME = process.env.HOST_PATHNAME || '' + let redirectUrl = `${HOST_PATHNAME}/#` + const { standaloneVolumes, niftiLayers, // deprecating - check if anyone calls this url @@ -127,7 +137,7 @@ module.exports = (query, _warningCb) => { regionsSelected, // deprecating - check if any one calls this url cRegionsSelected, - navigation, // deprecating - check if any one calls this endpoint + navigation, cNavigation, } = query || {} @@ -142,7 +152,6 @@ module.exports = (query, _warningCb) => { if (Object.values(WARNING_STRINGS).includes(arg)) _warningCb(arg) } - if (navigation) console.warn(`navigation has been deprecated`) if (regionsSelected) console.warn(`regionSelected has been deprecated`) if (niftiLayers) console.warn(`nifitlayers has been deprecated`) @@ -156,6 +165,30 @@ module.exports = (query, _warningCb) => { // common search param & path let nav, dsp, r + if (navigation) { + try { + + const [ + _o, _po, _pz, _p, _z + ] = navigation.split("__") + const o = _o.split("_").map(v => Number(v)) + const po = _po.split("_").map(v => Number(v)) + const pz = Number(_pz) + const p = _p.split("_").map(v => Number(v)) + const z = Number(_z) + const v = [ + o.map(n => encodeNumber(n, {float: true})).join(separator), + po.map(n => encodeNumber(n, {float: true})).join(separator), + encodeNumber(Math.floor(pz)), + Array.from(p).map(v => Math.floor(v)).map(n => encodeNumber(n)).join(separator), + encodeNumber(Math.floor(z)), + ].join(`${separator}${separator}`) + + nav = `/@:${encodeURI(v)}` + } catch (e) { + console.warn(`Parsing navigation param error`, e) + } + } if (cNavigation) nav = `/@:${encodeURI(cNavigation)}` if (previewingDatasetFiles) { try { @@ -163,7 +196,17 @@ module.exports = (query, _warningCb) => { if (Array.isArray(parsedDsp)) { if (parsedDsp.length === 1) { const { datasetId, filename } = parsedDsp[0] - dsp = `/dsp:${encodeId(datasetId)}::${encodeURI(filename)}` + if (datasetId === "minds/core/dataset/v1.0.0/b08a7dbc-7c75-4ce7-905b-690b2b1e8957") { + /** + * if preview pli link, return hardcoded link + */ + return `${HOST_PATHNAME}/#${pliPreviewUrl}` + } else { + /** + * TODO deprecate dsp + */ + dsp = `/dsp:${encodeId(datasetId)}::${encodeURI(filename)}` + } } else { searchParam.set(`previewingDatasetFiles`, previewingDatasetFiles) } @@ -206,8 +249,6 @@ module.exports = (query, _warningCb) => { // ignore region selected and move on } } - const HOST_PATHNAME = process.env.HOST_PATHNAME || '' - let redirectUrl = `${HOST_PATHNAME}/#` if (standaloneVolumes) { searchParam.set('standaloneVolumes', standaloneVolumes) if (nav) redirectUrl += nav diff --git a/deploy/csp/index.js b/deploy/csp/index.js index fcfa227d1687f5b52246f50634effd0ff6d0bbd8..9cc365a20323aad04d461fc9143c81f56cc91db4 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -54,7 +54,9 @@ const connectSrc = [ 'object.cscs.ch', // required for dataset previews - 'hbp-kg-dataset-previewer.apps.hbp.eu/v2/', + + // spatial transform + "hbp-spatial-backend.apps.hbp.eu", // injected by env var ...CSP_CONNECT_SRC @@ -102,14 +104,13 @@ module.exports = { ], imgSrc: [ "'self'", - "hbp-kg-dataset-previewer.apps.hbp.eu/v2/" ], scriptSrc:[ "'self'", ...userScriptSrc, 'unpkg.com/kg-dataset-previewer@1.2.0/', // preview component - 'cdnjs.cloudflare.com/ajax/libs/d3/', // required for preview component - 'cdnjs.cloudflare.com/ajax/libs/mathjax/', // math jax + 'https://unpkg.com/d3@6.2.0/', // required for preview component + 'https://unpkg.com/mathjax@3.1.2/', // math jax 'https://unpkg.com/three-surfer@0.0.11/dist/bundle.js', // for threeSurfer (freesurfer support in browser) 'https://unpkg.com/ng-layer-tune@0.0.6/dist/ng-layer-tune/', // needed for ng layer control 'https://unpkg.com/hbp-connectivity-component@0.6.5/', // needed for connectivity component @@ -118,6 +119,9 @@ module.exports = { ...WHITE_LIST_SRC, ...defaultAllowedSites ], + frameSrc: [ + "*" + ], reportUri: CSP_REPORT_URI || '/report-violation' }, reportOnly diff --git a/deploy/package-lock.json b/deploy/package-lock.json index 1dac062d78f8ca3d5a23d9456457a913ceef919c..85a12542dc7b71eca0510d03a3d5ce1178f09567 100644 --- a/deploy/package-lock.json +++ b/deploy/package-lock.json @@ -10,9 +10,9 @@ "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" }, "@sindresorhus/is": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-1.2.0.tgz", - "integrity": "sha512-mwhXGkRV5dlvQc4EgPDxDxO6WuMBVymGFd1CA+2Y+z5dG9MNspoQ+AWjl/Ld1MnpCL8AKbosZlDVohqcIwuWsw==" + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, "@sinonjs/commons": { "version": "1.7.0", @@ -924,25 +924,60 @@ } }, "got": { - "version": "10.5.5", - "resolved": "https://registry.npmjs.org/got/-/got-10.5.5.tgz", - "integrity": "sha512-B13HHkCkTA7KxyxTrFoZfrurBX1fZxjMTKpmIfoVzh0Xfs9aZV7xEfI6EKuERQOIPbomh5LE4xDkfK6o2VXksw==", + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", + "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", "requires": { - "@sindresorhus/is": "^1.0.0", - "@szmarczak/http-timer": "^4.0.0", + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", - "cacheable-lookup": "^2.0.0", - "cacheable-request": "^7.0.1", - "decompress-response": "^5.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^5.0.0", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", - "mimic-response": "^2.0.0", "p-cancelable": "^2.0.0", - "p-event": "^4.0.0", - "responselike": "^2.0.0", - "to-readable-stream": "^2.0.0", - "type-fest": "^0.9.0" + "responselike": "^2.0.0" + }, + "dependencies": { + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + } } }, "growl": { @@ -1571,9 +1606,9 @@ }, "dependencies": { "@sindresorhus/is": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz", - "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==" + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, "cacheable-lookup": { "version": "5.0.4", @@ -1589,21 +1624,46 @@ } }, "got": { - "version": "11.8.2", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", - "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", + "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", "requires": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.1", + "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" + }, + "dependencies": { + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + } } }, "lru-cache": { @@ -1619,6 +1679,11 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -1917,9 +1982,9 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "resolve-alpn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", - "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" }, "responselike": { "version": "2.0.0", @@ -2229,11 +2294,6 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, - "type-fest": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.9.0.tgz", - "integrity": "sha512-j55pzONIdg7rdtJTRZPKIbV0FosUqYdhHK1aAYJIrUvejv1VVyBokrILE8KQDT4emW/1Ev9tx+yZG+AxuSBMmA==" - }, "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", diff --git a/deploy/package.json b/deploy/package.json index 333568cdfc0b2804ed97319a6b00dd1577c59d50..9470dcf3c16f707ecd0c023fcff17e3e843d2bc8 100644 --- a/deploy/package.json +++ b/deploy/package.json @@ -18,7 +18,7 @@ "express": "^4.16.4", "express-rate-limit": "^5.5.1", "express-session": "^1.15.6", - "got": "^10.5.5", + "got": "^11.8.5", "hbp-seafile": "^0.2.0", "helmet-csp": "^3.4.0", "lru-cache": "^5.1.1", diff --git a/deploy/plugins/index.js b/deploy/plugins/index.js index 6089e2fe9eb3c54dcb8f4c081f947d4cb719377c..028a0158d9a94b4a353242099c9cfed7eb8f867f 100644 --- a/deploy/plugins/index.js +++ b/deploy/plugins/index.js @@ -35,40 +35,45 @@ const getKey = url => `plugin:manifest-cache:${url}` router.get('/manifests', async (_req, res) => { const allManifests = await Promise.all( - [...V2_7_PLUGIN_URLS, ...V2_7_STAGING_PLUGIN_URLS].map(async url => - race( - async () => { - const key = getKey(url) - - await lruStore._initPr - const { store } = lruStore - - try { - const storedManifest = await store.get(key) - if (storedManifest) return JSON.parse(storedManifest) - else throw `not found` - } catch (e) { - const resp = await got(url) - const json = JSON.parse(resp.body) - - const { iframeUrl, 'siibra-explorer': flag } = json - if (!flag) return null - if (!iframeUrl) return null - const u = new URL(url) + [...V2_7_PLUGIN_URLS, ...V2_7_STAGING_PLUGIN_URLS].map(async url => { + try { + return await race( + async () => { + const key = getKey(url) - let replaceObj = {} - if (!/^https?:\/\//.test(iframeUrl)) { - u.pathname = path.resolve(path.dirname(u.pathname), iframeUrl) - replaceObj['iframeUrl'] = u.toString() + await lruStore._initPr + const { store } = lruStore + + try { + const storedManifest = await store.get(key) + if (storedManifest) return JSON.parse(storedManifest) + else throw `not found` + } catch (e) { + const resp = await got(url) + const json = JSON.parse(resp.body) + + const { iframeUrl, 'siibra-explorer': flag } = json + if (!flag) return null + if (!iframeUrl) return null + const u = new URL(url) + + let replaceObj = {} + if (!/^https?:\/\//.test(iframeUrl)) { + u.pathname = path.resolve(path.dirname(u.pathname), iframeUrl) + replaceObj['iframeUrl'] = u.toString() + } + const returnObj = {...json, ...replaceObj} + await store.set(key, JSON.stringify(returnObj), { maxAge: 1000 * 60 * 60 }) + return returnObj } - const returnObj = {...json, ...replaceObj} - await store.set(key, JSON.stringify(returnObj), { maxAge: 1000 * 60 * 60 }) - return returnObj - } - }, - { timeout: 1000 } - ) - ) + }, + { timeout: 1000 } + ) + } catch (e) { + console.error(`fetching manifest error: ${e}`) + return null + } + }) ) res.status(200).json( diff --git a/deploy/saneUrl/index.js b/deploy/saneUrl/index.js index 3bc0bca9fe6e6eafad1075a0a4b16b2eddddd91e..582113e0c19cc9461c81eb288824e265e0d3bef1 100644 --- a/deploy/saneUrl/index.js +++ b/deploy/saneUrl/index.js @@ -104,7 +104,8 @@ router.post('/:name', limiterMiddleware(), express.json(), async (req, res) => { - if (req.headers['x-noop']) return res.status(200).end() + if (/bot/i.test(req.headers['user-agent'])) return res.status(201).end() + if (req.headers['x-noop']) return res.status(201).end() const { name } = req.params try { await proxyStore.set(req, name, req.body) diff --git a/deploy/util/reconfigPrecomputedServer.js b/deploy/util/reconfigPrecomputedServer.js deleted file mode 100644 index 98a1932ba74dbdc4361efae7056f876af7141b15..0000000000000000000000000000000000000000 --- a/deploy/util/reconfigPrecomputedServer.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * n.b. trailing slash is required - * e.g. http://localhost:10080/ - */ -const PRECOMPUTED_SERVER = process.env.PRECOMPUTED_SERVER - -const reconfigureFlag = !!PRECOMPUTED_SERVER - -exports.reconfigureFlag = reconfigureFlag - -exports.reconfigureUrl = (str) => { - if (!reconfigureFlag) return str - return str.replace(/https?:\/\/.*?\//g, PRECOMPUTED_SERVER) -} \ No newline at end of file diff --git a/deploy_env.md b/deploy_env.md index bb17c783cbd86d4fa1932a36f44f7a4118012452..1926d4bb3fb5214230399d416954165c7a9b7862 100644 --- a/deploy_env.md +++ b/deploy_env.md @@ -8,12 +8,14 @@ | `HOST_PATHNAME` | pathname to listen on, restrictions: leading slash, no trailing slash | `''` | `/viewer` | | `SESSIONSECRET` | session secret for cookie session | | `NODE_ENV` | determines where the built viewer will be served from | | `production` | -| `PRECOMPUTED_SERVER` | redirect data uri to another server. Useful for offline demos | | `http://localhost:8080/precomputed/` | +| ~~`PRECOMPUTED_SERVER`~~ _deprecated_, use `LOCAL_CDN` instead. | redirect data uri to another server. Useful for offline demos | | `http://localhost:8080/precomputed/` | | `LOCAL_CDN` | rewrite cdns to local server. useful for offlnie demo | | `http://localhost:7080/` | | `PLUGIN_URLS` | semi colon separated urls to be returned when user queries plugins | `''` | `STAGING_PLUGIN_URLS` | semi colon separated urls to be returned when user queries plugins | `''` | `USE_LOGO` | possible values are `hbp`, `ebrains`, `fzj` | `hbp` | `ebrains` | | `__DEBUG__` | debug flag | +| `BUILD_TEXT` | overlay text at bottom right of the viewer. set to `''` to hide. | | + ##### ebrains user authentication diff --git a/docs/releases/v2.6.10.md b/docs/releases/v2.6.10.md new file mode 100644 index 0000000000000000000000000000000000000000..afc4ab3ece8cdef9414ff7ef42ad7a84b4489007 --- /dev/null +++ b/docs/releases/v2.6.10.md @@ -0,0 +1,5 @@ +# 2.6.9 + +## Bugfix + +- Remove empty quick tour. diff --git a/docs/releases/v2.7.1.md b/docs/releases/v2.7.1.md new file mode 100644 index 0000000000000000000000000000000000000000..a2d967a9c06c5baa042653168ddded4bbf99c52f --- /dev/null +++ b/docs/releases/v2.7.1.md @@ -0,0 +1,5 @@ +# v2.7.1 + +## Bugfix + +- fixed region detail fetching using duplicated id as endpoint diff --git a/docs/releases/v2.7.2.md b/docs/releases/v2.7.2.md new file mode 100644 index 0000000000000000000000000000000000000000..2a5ada611284053bd629c4f6dc0ed4c765889b58 --- /dev/null +++ b/docs/releases/v2.7.2.md @@ -0,0 +1,12 @@ +# v2.7.2 + +## Feature + +- (re)introduced the parcellation info button + +## Bugfix + +- fix the position of quick tour panel of slice view panels +- fix the atlas selection logic. This should reduce 4xx/5xx calls significantly +- minor update to parcellation chip appearance +- clicking on feature badge now properly selects the feature diff --git a/docs/releases/v2.7.3.md b/docs/releases/v2.7.3.md new file mode 100644 index 0000000000000000000000000000000000000000..ec24dc460b6f4bc81b0faab9a78fa5eaf29ff948 --- /dev/null +++ b/docs/releases/v2.7.3.md @@ -0,0 +1,13 @@ +# v2.7.3 + +## Bugfix + +- fixed matomo visitor counting (broke since 2.7.0 release) +- fixed sane url generation +- fixed reset navigation buttons in navigation card + +## Under the hood + +- minor refactor of unused code +- added mirrors to siibra-api +- experimental support for drag and drop swc diff --git a/docs/releases/v2.7.4.md b/docs/releases/v2.7.4.md new file mode 100644 index 0000000000000000000000000000000000000000..d214b2f110f3848759ec885f36821072f051b987 --- /dev/null +++ b/docs/releases/v2.7.4.md @@ -0,0 +1,6 @@ +# v2.7.4 + +## Bugfix + +- Properly use fallback when detecting fault +- Minor wording/cosmetic change diff --git a/docs/releases/v2.7.5.md b/docs/releases/v2.7.5.md new file mode 100644 index 0000000000000000000000000000000000000000..9ce31069890e6e1b5dea48881aba172bd4f0cc66 --- /dev/null +++ b/docs/releases/v2.7.5.md @@ -0,0 +1,24 @@ +# v2.7.5 + +## Features + +- experimental support for parsing some swc files. +- added toggle crosshair in helper one pager +- allow navigation to VOI's native orientation plane +- reworked plugin window + - it now longer act as if it is a separate window + - there are now three forms of the plugin: normal (default), maximized, minimized. + +## Bugfix + +- saneUrl were not able to correctly save user requests + +## Under the hood + +- enhancement in `strict local mode` + - hide external links in `strict local mode` + - do not try to perform interspace translation + - do not try to show feature pane +- direct support for Leap motion controller +- set git hash and version during build step (no longer requiring it to be set during build-arg step) +- restores the functionality to parse `navigation` query param diff --git a/e2e/checklist.md b/e2e/checklist.md index b51cd1cdc38ce354cf1bc0ca97a6d06e9b3896b4..fe9d0cb0fa85b4f5eab53e808f4cec5ba91f7bae 100644 --- a/e2e/checklist.md +++ b/e2e/checklist.md @@ -14,6 +14,11 @@ - [ ] Human multilevel atlas - [ ] on click from home page, MNI152, Julich v2.9 loads without issue - [ ] on hover, show correct region name(s) + - [ ] Parcellation smart chip + - [ ] show/hide parcellation toggle exists and works + - [ ] `q` is a shortcut to show/hide parcellation toggle + - [ ] info button exists and works + - [ ] info button shows desc, and link to KG - [ ] regional is fine :: select hOC1 right - [ ] probabilistic map loads fine - [ ] segmentation layer hides @@ -60,6 +65,10 @@ - [ ] on hover, show correct region name(s) - [ ] whole mesh loads ## saneURL +- [ ] saneurl generation functions properly + - [ ] try existing key (human), and get unavailable error + - [ ] try non existing key, and get available + - [ ] create use key `x-tmp-foo` and new url works - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/bigbrainGreyWhite) redirects to big brain - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/julichbrain) redirects to julich brain (colin 27) - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/whs4) redirects to waxholm v4 @@ -72,3 +81,4 @@ - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/mouse) redirects allen mouse 2017 ## plugins - [ ] jugex plugin works +- [ ] 1um section works diff --git a/mkdocs.yml b/mkdocs.yml index 189e32dd064493291664f3af1487024f0294f79a..5372b5da4bf778115d93f5ac54c3894ab8141b15 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: Interactive Atlas Viewer User Documentation +site_name: Siibra Explorer User Documentation theme: name: 'material' @@ -33,7 +33,13 @@ nav: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.7.5: 'releases/v2.7.5.md' + - v2.7.4: 'releases/v2.7.4.md' + - v2.7.3: 'releases/v2.7.3.md' + - v2.7.2: 'releases/v2.7.2.md' + - v2.7.1: 'releases/v2.7.1.md' - v2.7.0: 'releases/v2.7.0.md' + - v2.6.10: 'releases/v2.6.10.md' - v2.6.9: 'releases/v2.6.9.md' - v2.6.8: 'releases/v2.6.8.md' - v2.6.7: 'releases/v2.6.7.md' diff --git a/package.json b/package.json index 7f4703d9003a841fe409c5fdc906ceab3b9fa160..64e82a9d397bf02c29c67f449ffd8467b89b1f2d 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,7 @@ { "name": "interactive-viewer", - "version": "2.7.0", + "version": "2.7.5", "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", - "version": "2.7.0", "scripts": { "lint": "eslint src --ext .ts", "eslint": "eslint", diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts index ec352e5fbd3707b63b4da42ca92e9157c36bfb0c..fd67601ee087cf32ddd230d7e42b3ca9fdaea421 100644 --- a/src/atlasComponents/annotations/annotation.service.ts +++ b/src/atlasComponents/annotations/annotation.service.ts @@ -146,7 +146,8 @@ export class AnnotationLayer { } } updateAnnotation(spec: AnnotationSpec) { - const localAnnotations = this.nglayer.layer.localAnnotations + const localAnnotations = this.nglayer?.layer?.localAnnotations + if (!localAnnotations) return const ref = localAnnotations.references.get(spec.id) const _spec = this.parseNgSpecType(spec) if (ref) { diff --git a/src/atlasComponents/sapi/constants.ts b/src/atlasComponents/sapi/constants.ts index e9822b4a39c4157b893cef83f6d11082a8475674..b31cfc1b14703de11f2c5e2efaee1cacb369347b 100644 --- a/src/atlasComponents/sapi/constants.ts +++ b/src/atlasComponents/sapi/constants.ts @@ -1,13 +1,16 @@ export const IDS = { ATLAES: { - HUMAN: "juelich/iav/atlas/v1.0.0/1" + HUMAN: "juelich/iav/atlas/v1.0.0/1", + RAT: "minds/core/parcellationatlas/v1.0.0/522b368e-49a3-49fa-88d3-0870a307974a", }, TEMPLATES: { BIG_BRAIN: "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588", MNI152: "minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2", - COLIN27: "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992" + COLIN27: "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992", + WAXHOLM: "minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8", }, PARCELLATION: { - JBA29: "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290" + JBA29: "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290", + WAXHOLMV4: "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4", } } diff --git a/src/atlasComponents/sapi/core/sapiParcellation.ts b/src/atlasComponents/sapi/core/sapiParcellation.ts index 4767cc2897ab0ed6c6567f3ee727ad2f73fece04..085c5a82b8136901136593aa0b2933d992efb22e 100644 --- a/src/atlasComponents/sapi/core/sapiParcellation.ts +++ b/src/atlasComponents/sapi/core/sapiParcellation.ts @@ -1,4 +1,5 @@ import { Observable } from "rxjs" +import { switchMap } from "rxjs/operators" import { SapiVolumeModel } from ".." import { SAPI } from "../sapi.service" import {SapiParcellationFeatureModel, SapiParcellationModel, SapiQueryPriorityArg, SapiRegionModel} from "../type" @@ -20,43 +21,53 @@ export class SAPIParcellation{ } getDetail(queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationModel>{ - return this.sapi.httpGet<SapiParcellationModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`, - null, - queryParam + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiParcellationModel>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`, + null, + queryParam + )) ) } getRegions(spaceId: string, queryParam?: SapiQueryPriorityArg): Observable<SapiRegionModel[]> { - return this.sapi.httpGet<SapiRegionModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`, - { - space_id: spaceId - }, - queryParam + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiRegionModel[]>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`, + { + space_id: spaceId + }, + queryParam + )) ) } getVolumes(): Observable<SapiVolumeModel[]>{ - return this.sapi.httpGet<SapiVolumeModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes` - ) + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiVolumeModel[]>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes` + )) + ) } getFeatures(parcPagination?: ParcellationPaginationQuery, queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationFeatureModel[]> { - return this.sapi.httpGet<SapiParcellationFeatureModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`, - { - type: parcPagination?.type, - size: parcPagination?.size?.toString() || '5', - page: parcPagination?.page.toString() || '0', - }, - queryParam + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiParcellationFeatureModel[]>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`, + { + type: parcPagination?.type, + size: parcPagination?.size?.toString() || '5', + page: parcPagination?.page.toString() || '0', + }, + queryParam + )) ) } getFeatureInstance(instanceId: string): Observable<SapiParcellationFeatureModel> { - return this.sapi.http.get<SapiParcellationFeatureModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.http.get<SapiParcellationFeatureModel>( + `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, + )) ) } } diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts index 8248992c16ef86a1bbd6e28c395b8dd08db9d66f..d9263cea03f0d7045e3a1f5043f8a4edc2209929 100644 --- a/src/atlasComponents/sapi/core/sapiRegion.ts +++ b/src/atlasComponents/sapi/core/sapiRegion.ts @@ -2,7 +2,7 @@ import { SAPI } from ".."; import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset, SapiVolumeModel, PaginatedResponse } from "../type"; import { strToRgb, hexToRgb } from 'common/util' import { merge, Observable, of } from "rxjs"; -import { catchError, map, scan } from "rxjs/operators"; +import { catchError, map, scan, switchMap } from "rxjs/operators"; export class SAPIRegion{ @@ -16,7 +16,7 @@ export class SAPIRegion{ return strToRgb(JSON.stringify(region)) } - private prefix: string + private prefix$: Observable<string> constructor( private sapi: SAPI, @@ -24,20 +24,26 @@ export class SAPIRegion{ public parcId: string, public id: string, ){ - this.prefix = `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}` + this.prefix$ = SAPI.BsEndpoint$.pipe( + map(endpt => `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}`) + ) } getFeatures(spaceId: string): Observable<(SapiRegionalFeatureModel | CleanedIeegDataset)[]> { return merge( - this.sapi.httpGet<SapiRegionalFeatureModel[]>( - `${this.prefix}/features`, - { - space_id: spaceId - } - ).pipe( - catchError((err, obs) => { - return of([]) - }) + this.prefix$.pipe( + switchMap(prefix => + this.sapi.httpGet<SapiRegionalFeatureModel[]>( + `${prefix}/features`, + { + space_id: spaceId + } + ).pipe( + catchError((err, obs) => { + return of([]) + }) + ) + ) ), spaceId ? this.sapi.getSpace(this.atlasId, spaceId).getFeatures({ parcellationId: this.parcId, region: this.id }).pipe( @@ -56,50 +62,59 @@ export class SAPIRegion{ } getFeatureInstance(instanceId: string, spaceId: string = null): Observable<SapiRegionalFeatureModel> { - return this.sapi.httpGet<SapiRegionalFeatureModel>( - `${this.prefix}/features/${encodeURIComponent(instanceId)}`, - { - space_id: spaceId - } + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiRegionalFeatureModel>( + `${prefix}/features/${encodeURIComponent(instanceId)}`, + { + space_id: spaceId + } + )) ) } getMapInfo(spaceId: string): Observable<SapiRegionMapInfoModel> { - return this.sapi.http.get<SapiRegionMapInfoModel>( - `${this.prefix}/regional_map/info`, - { - params: { - space_id: spaceId + return this.prefix$.pipe( + switchMap(prefix => this.sapi.http.get<SapiRegionMapInfoModel>( + `${prefix}/regional_map/info`, + { + params: { + space_id: spaceId + } } - } + )) ) } - getMapUrl(spaceId: string): string { - return `${this.prefix}/regional_map/map?space_id=${encodeURI(spaceId)}` + getMapUrl(spaceId: string): Observable<string> { + return this.prefix$.pipe( + map(prefix => `${prefix}/regional_map/map?space_id=${encodeURI(spaceId)}`) + ) } getVolumes(): Observable<PaginatedResponse<SapiVolumeModel>>{ - const url = `${this.prefix}/volumes` - return this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>( - url + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>( + `${prefix}/volumes` + )) ) } getVolumeInstance(volumeId: string): Observable<SapiVolumeModel> { - const url = `${this.prefix}/volumes/${encodeURIComponent(volumeId)}` - return this.sapi.httpGet<SapiVolumeModel>( - url + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiVolumeModel>( + `${prefix}/volumes/${encodeURIComponent(volumeId)}` + )) ) } getDetail(spaceId: string): Observable<SapiRegionModel> { - const url = `${this.prefix}/${encodeURIComponent(this.id)}` - return this.sapi.httpGet<SapiRegionModel>( - url, - { - space_id: spaceId - } + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiRegionModel>( + prefix, + { + space_id: spaceId + } + )) ) } } diff --git a/src/atlasComponents/sapi/core/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts index 3effd6f16345aa82eebd3cf94b9f83118fb9546e..5f61ae6f68240437d2a9510b4744d64d16e54977 100644 --- a/src/atlasComponents/sapi/core/sapiSpace.ts +++ b/src/atlasComponents/sapi/core/sapiSpace.ts @@ -2,6 +2,7 @@ import { Observable } from "rxjs" import { SAPI } from '../sapi.service' import { camelToSnake } from 'common/util' import {SapiQueryPriorityArg, SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel} from "../type" +import { map, switchMap } from "rxjs/operators" type FeatureResponse = { features: { @@ -22,13 +23,21 @@ type SpatialFeatureOpts = RegionalSpatialFeatureOpts | BBoxSpatialFEatureOpts export class SAPISpace{ - constructor(private sapi: SAPI, public atlasId: string, public id: string){} + constructor(private sapi: SAPI, public atlasId: string, public id: string){ + this.prefix$ = SAPI.BsEndpoint$.pipe( + map(endpt => `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`) + ) + } + + private prefix$: Observable<string> getModalities(param?: SapiQueryPriorityArg): Observable<FeatureResponse> { - return this.sapi.httpGet<FeatureResponse>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`, - null, - param + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<FeatureResponse>( + `${prefix}/features`, + null, + param + )) ) } @@ -37,9 +46,11 @@ export class SAPISpace{ for (const [key, value] of Object.entries(opts)) { query[camelToSnake(key)] = value } - return this.sapi.httpGet<SapiSpatialFeatureModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`, - query + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel[]>( + `${prefix}/features`, + query + )) ) } @@ -48,23 +59,29 @@ export class SAPISpace{ for (const [key, value] of Object.entries(opts)) { query[camelToSnake(key)] = value } - return this.sapi.httpGet<SapiSpatialFeatureModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`, - query + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel>( + `${prefix}/features/${encodeURIComponent(instanceId)}`, + query + )) ) } getDetail(param?: SapiQueryPriorityArg): Observable<SapiSpaceModel>{ - return this.sapi.httpGet<SapiSpaceModel>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`, - null, - param + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiSpaceModel>( + `${prefix}`, + null, + param + )) ) } getVolumes(): Observable<SapiVolumeModel[]>{ - return this.sapi.httpGet<SapiVolumeModel[]>( - `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/volumes`, + return this.prefix$.pipe( + switchMap(prefix => this.sapi.httpGet<SapiVolumeModel[]>( + `${prefix}/volumes`, + )) ) } } diff --git a/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts b/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts index 1a0c0c690be0eb7615922a8068ffc86422af54fb..8dff3f67d6bbc69939068718270e487ee3bbeb2c 100644 --- a/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts +++ b/src/atlasComponents/sapi/core/space/interSpaceCoordXform.service.ts @@ -57,6 +57,12 @@ export class InterSpaceCoordXformSvc { // see https://github.com/ngrx/platform/issues/498#issuecomment-337465179 // in order to properly test with marble, use obs instead of promise transform(srcTmplName: ValidTemplateSpaceName, targetTmplName: ValidTemplateSpaceName, coordinatesInNm: [number, number, number]): Observable<ITemplateCoordXformResp> { + if (environment.STRICT_LOCAL) { + return of({ + status: 'error', + statusText: 'STRICT_LOCAL mode on, will not transform' + } as ITemplateCoordXformResp) + } if (!srcTmplName || !targetTmplName) { return of({ status: 'error', diff --git a/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts b/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts new file mode 100644 index 0000000000000000000000000000000000000000..44c93c74186567e3967d2224cb4937ee391de215 --- /dev/null +++ b/src/atlasComponents/sapi/core/space/interspaceLinearXform.ts @@ -0,0 +1,60 @@ +export const VALID_LINEAR_XFORM_SRC = { + CCF: "Allen Common Coordination Framework" +} + +export const VALID_LINEAR_XFORM_DST = { + NEHUBA: "nehuba" +} + +export type TVALID_LINEAR_XFORM_SRC = keyof typeof VALID_LINEAR_XFORM_SRC +export type TVALID_LINEAR_XFORM_DST = keyof typeof VALID_LINEAR_XFORM_DST + +type TLinearXform = number[][] + +const _linearXformDict: Record< + keyof typeof VALID_LINEAR_XFORM_SRC, + Record< + keyof typeof VALID_LINEAR_XFORM_DST, + TLinearXform + >> = { + CCF: { + NEHUBA: [ + [-1e3, 0, 0, 11400000 - 5737500], // + [0, 0, -1e3, 13200000 - 6637500], // + [0, -1e3, 0, 8000000 - 4037500], // + [0, 0, 0, 1], + ] + } + } + +const defaultXform = [ + [1e3, 0, 0, 0], + [0, 1e3, 0, 0], + [0, 0, 1e3, 0], + [0, 0, 0, 1], +] + + +const getProxyXform = <T>(obj: Record<string, T>, cb: (value: T) => T) => new Proxy({}, { + get: (_target, prop: string, _receiver) => { + return cb(obj[prop]) + } +}) + +export const linearXformDict = getProxyXform(_linearXformDict, (value: Record<string, TLinearXform>) => { + if (!value) return getProxyXform({}, () => defaultXform) + return getProxyXform(value, (v: TLinearXform) => { + if (v) return v + return defaultXform + }) +}) as Record< + keyof typeof VALID_LINEAR_XFORM_SRC, + Record< + keyof typeof VALID_LINEAR_XFORM_DST, + TLinearXform + >> + + +export const linearTransform = async (srcTmplName: keyof typeof VALID_LINEAR_XFORM_SRC, targetTmplName: keyof typeof VALID_LINEAR_XFORM_DST) => { + return linearXformDict[srcTmplName][targetTmplName] +} diff --git a/src/atlasComponents/sapi/features/sapiFeature.ts b/src/atlasComponents/sapi/features/sapiFeature.ts index 5abaa0040f6cb71046c70bf57e7a77a57f2794a8..f2f341acce3aca72ba481f3eda6c68083320cfe1 100644 --- a/src/atlasComponents/sapi/features/sapiFeature.ts +++ b/src/atlasComponents/sapi/features/sapiFeature.ts @@ -1,3 +1,4 @@ +import { switchMap } from "rxjs/operators"; import { SAPI } from "../sapi.service"; import { SapiFeatureModel } from "../type"; @@ -6,8 +7,10 @@ export class SAPIFeature { } - public detail$ = this.sapi.httpGet<SapiFeatureModel>( - `${SAPI.bsEndpoint}/features/${this.id}`, - this.opts + public detail$ = SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.sapi.httpGet<SapiFeatureModel>( + `${endpt}/features/${this.id}`, + this.opts + )) ) } diff --git a/src/atlasComponents/sapi/module.ts b/src/atlasComponents/sapi/module.ts index a64cc8bc817f05c801cde40d58a95d93c1d198a1..9b9efaf0ee92240c0b27b57b5067acf02dcbd972 100644 --- a/src/atlasComponents/sapi/module.ts +++ b/src/atlasComponents/sapi/module.ts @@ -1,5 +1,4 @@ import { NgModule } from "@angular/core"; -import { SAPI } from "./sapi.service"; import { CommonModule } from "@angular/common"; import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; import { PriorityHttpInterceptor } from "src/util/priority"; @@ -16,7 +15,6 @@ import { MatSnackBarModule } from "@angular/material/snack-bar"; exports: [ ], providers: [ - SAPI, { provide: HTTP_INTERCEPTORS, useClass: PriorityHttpInterceptor, diff --git a/src/atlasComponents/sapi/sapi.service.spec.ts b/src/atlasComponents/sapi/sapi.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..20233eca63ce2485db0136ee3702cace46659e96 --- /dev/null +++ b/src/atlasComponents/sapi/sapi.service.spec.ts @@ -0,0 +1,107 @@ +import { finalize } from "rxjs/operators" +import * as env from "src/environments/environment" +import { SAPI } from "./sapi.service" + +describe("> sapi.service.ts", () => { + describe("> SAPI", () => { + describe("#BsEndpoint$", () => { + let fetchSpy: jasmine.Spy + let environmentSpy: jasmine.Spy + + const endpt1 = 'http://foo-bar' + const endpt2 = 'http://buzz-bizz' + + const atlas1 = 'foo' + const atlas2 = 'bar' + + let subscribedVal: string + + beforeEach(() => { + SAPI.ClearBsEndPoint() + fetchSpy = spyOn(window, 'fetch') + fetchSpy.and.callThrough() + + environmentSpy = spyOnProperty(env, 'environment') + environmentSpy.and.returnValue({ + SIIBRA_API_ENDPOINTS: `${endpt1},${endpt2}` + }) + }) + + + afterEach(() => { + SAPI.ClearBsEndPoint() + fetchSpy.calls.reset() + environmentSpy.calls.reset() + subscribedVal = null + }) + + describe("> first passes", () => { + beforeEach(done => { + const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 }) + fetchSpy.and.callFake(async url => { + if (url === `${endpt1}/atlases`) { + return resp + } + throw new Error("controlled throw") + }) + SAPI.BsEndpoint$.pipe( + finalize(() => done()) + ).subscribe(val => { + subscribedVal = val + }) + }) + it("> should call fetch twice", async () => { + expect(fetchSpy).toHaveBeenCalledTimes(2) + + const allArgs = fetchSpy.calls.allArgs() + expect(allArgs.length).toEqual(2) + expect(allArgs[0]).toEqual([`${endpt1}/atlases`]) + expect(allArgs[1]).toEqual([`${endpt2}/atlases`]) + }) + + it("> endpoint should be set", async () => { + expect(subscribedVal).toBe(endpt1) + }) + + it("> additional calls should return cached observable", () => { + + expect(fetchSpy).toHaveBeenCalledTimes(2) + SAPI.BsEndpoint$.subscribe() + SAPI.BsEndpoint$.subscribe() + + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) + }) + + describe("> first fails", () => { + beforeEach(done => { + fetchSpy.and.callFake(async url => { + if (url === `${endpt1}/atlases`) { + throw new Error(`bla`) + } + const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 }) + return resp + }) + + SAPI.BsEndpoint$.pipe( + finalize(() => done()) + ).subscribe(val => { + subscribedVal = val + }) + }) + + it("> should call twice", async () => { + expect(fetchSpy).toHaveBeenCalledTimes(2) + expect(fetchSpy.calls.allArgs()).toEqual([ + [`${endpt1}/atlases`], + [`${endpt2}/atlases`], + ]) + }) + + it('> should set endpt2', async () => { + expect(subscribedVal).toBe(endpt2) + }) + }) + }) + }) +}) diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts index 62ba0e2528a8633db52bc44acdcadd3e9e851e04..ef3dc46e1e4dd37e1b50206f5b070bc562a270e5 100644 --- a/src/atlasComponents/sapi/sapi.service.ts +++ b/src/atlasComponents/sapi/sapi.service.ts @@ -1,9 +1,10 @@ import { Injectable } from "@angular/core"; import { HttpClient } from '@angular/common/http'; -import { map, shareReplay } from "rxjs/operators"; +import { catchError, filter, map, shareReplay, switchMap, take, tap } from "rxjs/operators"; import { SAPIAtlas, SAPISpace } from './core' import { - SapiAtlasModel, SapiModalityModel, + SapiAtlasModel, + SapiModalityModel, SapiParcellationModel, SapiQueryPriorityArg, SapiRegionalFeatureModel, @@ -19,20 +20,65 @@ import { MatSnackBar } from "@angular/material/snack-bar"; import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; import { EnumColorMapName } from "src/util/colorMaps"; import { PRIORITY_HEADER } from "src/util/priority"; -import { Observable } from "rxjs"; +import { concat, EMPTY, from, merge, Observable, of } from "rxjs"; import { SAPIFeature } from "./features"; import { environment } from "src/environments/environment" export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version' -export const SIIBRA_API_VERSION = '0.2.0' +export const SIIBRA_API_VERSION = '0.2.2' type RegistryType = SAPIAtlas | SAPISpace | SAPIParcellation -@Injectable() +let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null + +@Injectable({ + providedIn: 'root' +}) export class SAPI{ - static bsEndpoint = environment.BS_REST_URL || `https://siibra-api-latest.apps-dev.hbp.eu/v2_0` - public bsEndpoint = SAPI.bsEndpoint + /** + * Used to clear BsEndPoint, so the next static get BsEndpoints$ will + * fetch again. Only used for unit test of BsEndpoint$ + */ + static ClearBsEndPoint(){ + BS_ENDPOINT_CACHED_VALUE = null + } + + /** + * BsEndpoint$ is designed as a static getter mainly for unit testing purposes. + * see usage of BsEndpoint$ and ClearBsEndPoint in sapi.service.spec.ts + */ + static get BsEndpoint$(): Observable<string> { + if (!!BS_ENDPOINT_CACHED_VALUE) return BS_ENDPOINT_CACHED_VALUE + BS_ENDPOINT_CACHED_VALUE = concat( + merge( + ...environment.SIIBRA_API_ENDPOINTS.split(',').map(url => { + return from((async () => { + const resp = await fetch(`${url}/atlases`) + const atlases = await resp.json() + if (atlases.length == 0) { + throw new Error(`atlas length == 0`) + } + return url + })()).pipe( + catchError(() => EMPTY) + ) + }) + ), + of(null).pipe( + tap(() => { + SAPI.ErrorMessage = `It appears all of our mirrors are not working. The viewer may not be working properly...` + }), + filter(() => false) + ) + ).pipe( + take(1), + shareReplay(1), + ) + return BS_ENDPOINT_CACHED_VALUE + } + + static ErrorMessage = null registry = { _map: {} as Record<string, { @@ -92,7 +138,9 @@ export class SAPI{ } getModalities(): Observable<SapiModalityModel[]> { - return this.http.get<SapiModalityModel[]>(`${SAPI.bsEndpoint}/modalities`) + return SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.http.get<SapiModalityModel[]>(`${endpt}/modalities`)) + ) } httpGet<T>(url: string, params?: Record<string, string>, sapiParam?: SapiQueryPriorityArg){ @@ -109,12 +157,13 @@ export class SAPI{ ) } - public atlases$ = this.http.get<SapiAtlasModel[]>( - `${this.bsEndpoint}/atlases`, - { - observe: "response" - } - ).pipe( + public atlases$ = SAPI.BsEndpoint$.pipe( + switchMap(endpt => this.http.get<SapiAtlasModel[]>( + `${endpt}/atlases`, + { + observe: "response" + } + )), map(resp => { const respVersion = resp.headers.get(SIIBRA_API_VERSION_HEADER_KEY) if (respVersion !== SIIBRA_API_VERSION) { @@ -133,6 +182,9 @@ export class SAPI{ private snackbar: MatSnackBar, private workerSvc: AtlasWorkerService, ){ + if (SAPI.ErrorMessage) { + this.snackbar.open(SAPI.ErrorMessage, 'Dismiss', { duration: 5000 }) + } this.atlases$.subscribe(atlases => { for (const atlas of atlases) { for (const space of atlas.spaces) { diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts index 51fde4f0cd5695f6055398e8bd9ec39ed469c541..57e28f20da83acf96a247117f70fb921998c3bf0 100644 --- a/src/atlasComponents/sapi/stories.base.ts +++ b/src/atlasComponents/sapi/stories.base.ts @@ -67,22 +67,27 @@ export const parcId = { } export async function getAtlases(): Promise<SapiAtlasModel[]> { - return await (await fetch(`${SAPI.bsEndpoint}/atlases`)).json() as SapiAtlasModel[] + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases`)).json() as SapiAtlasModel[] } export async function getAtlas(id: string): Promise<SapiAtlasModel>{ - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${id}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${id}`)).json() } export async function getParc(atlasId: string, id: string): Promise<SapiParcellationModel>{ - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/parcellations/${id}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}`)).json() } export async function getParcRegions(atlasId: string, id: string, spaceId: string): Promise<SapiRegionModel[]>{ - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json() } export async function getSpace(atlasId: string, id: string): Promise<SapiSpaceModel> { - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId}/spaces/${id}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId}/spaces/${id}`)).json() } export async function getHumanAtlas(): Promise<SapiAtlasModel> { @@ -90,7 +95,8 @@ export async function getHumanAtlas(): Promise<SapiAtlasModel> { } export async function getMni152(): Promise<SapiSpaceModel> { - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}`)).json() } export async function getJba29(): Promise<SapiParcellationModel> { @@ -103,33 +109,41 @@ export async function getJba29Regions(): Promise<SapiRegionModel[]> { export async function getHoc1Right(spaceId=null): Promise<SapiRegionModel> { if (!spaceId) { - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right`)).json() } - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json() } export async function get44Left(spaceId=null): Promise<SapiRegionModel> { if (!spaceId) { - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json() } - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json() } export async function getHoc1RightSpatialFeatures(): Promise<SxplrCleanedFeatureModel[]> { - const json: SapiSpatialFeatureModel[] = await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features?parcellation_id=2.9®ion=hoc1%20right`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + const json: SapiSpatialFeatureModel[] = await (await fetch(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features?parcellation_id=2.9®ion=hoc1%20right`)).json() return cleanIeegSessionDatasets(json.filter(it => it['@type'] === "siibra/features/ieegSession")) } export async function getHoc1RightFeatures(): Promise<SapiRegionalFeatureModel[]> { - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features`)).json() } export async function getHoc1RightFeatureDetail(featId: string): Promise<SapiRegionalFeatureModel>{ - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features/${encodeURIComponent(featId)}`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features/${encodeURIComponent(featId)}`)).json() } export async function getJba29Features(): Promise<SapiParcellationFeatureModel[]> { - return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json() + const endPt = await SAPI.BsEndpoint$.toPromise() + return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json() } export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureModel[]>{ @@ -137,14 +151,16 @@ export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureMo [-1000, -1000, -1000], [1000, 1000, 1000] ] - const url = new URL(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.bigbrain}/features`) + const endPt = await SAPI.BsEndpoint$.toPromise() + const url = new URL(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.bigbrain}/features`) url.searchParams.set(`bbox`, JSON.stringify(bbox)) return await (await fetch(url.toString())).json() } export async function getMni152SpatialFeatureHoc1Right(): Promise<SapiSpatialFeatureModel[]>{ - const url = new URL(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features`) + const endPt = await SAPI.BsEndpoint$.toPromise() + const url = new URL(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features`) url.searchParams.set(`parcellation_id`, parcId.human.jba29) url.searchParams.set("region", 'hoc1 right') return await (await fetch(url.toString())).json() diff --git a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts index 5636ccdd3e36c4930bab5a923f1b20d4ec25507a..58a2151732937cd390d516ace5a2bfab2de87f7b 100644 --- a/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts +++ b/src/atlasComponents/sapiViews/core/atlas/tmplParcSelector/tmplParcSelector.component.ts @@ -104,13 +104,20 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector { ) ) - private showOverlayIntent$ = new Subject() + private showOverlayIntentByTemplate$ = new Subject() + private showOverlayIntentByParcellation$ = new Subject() public showLoadingOverlay$ = merge( - this.showOverlayIntent$.pipe( + this.showOverlayIntentByTemplate$.pipe( mapTo(true) ), this.selectedTemplate$.pipe( mapTo(false) + ), + this.showOverlayIntentByParcellation$.pipe( + mapTo(true) + ), + this.selectedParcellation$.pipe( + mapTo(false) ) ).pipe( distinctUntilChanged(), @@ -180,7 +187,7 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector { } selectTemplate(tmpl: SapiSpaceModel) { - this.showOverlayIntent$.next(true) + this.showOverlayIntentByTemplate$.next(true) this.store$.dispatch( atlasSelection.actions.selectTemplate({ @@ -190,6 +197,7 @@ export class SapiViewsCoreAtlasAtlasTmplParcSelector { } selectParcellation(parc: SapiParcellationModel) { + this.showOverlayIntentByParcellation$.next(true) this.store$.dispatch( atlasSelection.actions.selectParcellation({ diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts index 08bdb9d55bd85a616914cae076797142f3ff1026..22813095a37d1c1b8b6a12b44003995d8b7e8db5 100644 --- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts +++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.component.ts @@ -1,15 +1,29 @@ -import { Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from "@angular/core"; import { SapiDatasetModel } from "src/atlasComponents/sapi"; +import { CONST } from "common/constants" + +const RESTRICTED_ACCESS_ID = "https://nexus.humanbrainproject.org/v0/data/minds/core/embargostatus/v1.0.0/3054f80d-96a8-4dce-9b92-55c68a8b5efd" @Component({ selector: `sxplr-sapiviews-core-datasets-dataset`, templateUrl: './dataset.template.html', styleUrls: [ `./dataset.style.css` - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) -export class DatasetView { +export class DatasetView implements OnChanges{ @Input('sxplr-sapiviews-core-datasets-dataset-input') dataset: SapiDatasetModel + + public isRestricted = false + public CONST = CONST + + ngOnChanges(changes: SimpleChanges): void { + const { dataset } = changes + if (dataset) { + this.isRestricted = (dataset.currentValue as SapiDatasetModel)?.metadata?.accessibility?.["@id"] === RESTRICTED_ACCESS_ID + } + } } diff --git a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html index 68ce4f9a8540b0c1154408328260ed2e00a07b5a..a04996afbb8d961cba682ed0cd1cc9d5b2940d31 100644 --- a/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html +++ b/src/atlasComponents/sapiViews/core/datasets/dataset/dataset.template.html @@ -24,9 +24,16 @@ <span class="sxplr-m-a"> EBRAINS dataset </span> + + <button *ngIf="isRestricted" + [matTooltip]="CONST.GDPR_TEXT" + mat-icon-button color="warn"> + <i class="fas fa-exclamation-triangle"></i> + </button> + <mat-divider class="sxplr-pl-1" [vertical]="true"></mat-divider> - <a mat-icon-button *ngFor="let url of dataset.urls" [href]="url.doi | parseDoi" target="_blank"> + <a mat-icon-button sxplr-hide-when-local *ngFor="let url of dataset.urls" [href]="url.doi | parseDoi" target="_blank"> <i class="fas fa-external-link-alt"></i> </a> </mat-card-subtitle> diff --git a/src/atlasComponents/sapiViews/core/datasets/module.ts b/src/atlasComponents/sapiViews/core/datasets/module.ts index b295da6bc25f1a9fdc98031a42810ba907013320..d7b43955b58f954fa02bbbf9000982e5ac382b41 100644 --- a/src/atlasComponents/sapiViews/core/datasets/module.ts +++ b/src/atlasComponents/sapiViews/core/datasets/module.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { MarkdownModule } from "src/components/markdown"; import { AngularMaterialModule } from "src/sharedModules"; +import { StrictLocalModule } from "src/strictLocal"; import { SapiViewsUtilModule } from "../../util/module"; import { DatasetView } from "./dataset/dataset.component"; @@ -10,7 +11,8 @@ import { DatasetView } from "./dataset/dataset.component"; CommonModule, AngularMaterialModule, MarkdownModule, - SapiViewsUtilModule + SapiViewsUtilModule, + StrictLocalModule, ], declarations: [ DatasetView, diff --git a/src/atlasComponents/sapiViews/core/index.ts b/src/atlasComponents/sapiViews/core/index.ts index 6db4628176e571f0a50adca091dc02c5d51801de..cb3d0ffce18ea93d534e44866392f0fc819b0ae8 100644 --- a/src/atlasComponents/sapiViews/core/index.ts +++ b/src/atlasComponents/sapiViews/core/index.ts @@ -1,3 +1,7 @@ export { SapiViewsCoreModule -} from "./module" \ No newline at end of file +} from "./module" + +export { + SapiViewsCoreSpaceBoundingBox +} from "./space" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/core/parcellation/module.ts b/src/atlasComponents/sapiViews/core/parcellation/module.ts index fca2367997a445d3ccb20593a6ce44cf3f1f9283..1fe2e70c09d9a1bda3e3fd3a991d76eac75be09b 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/module.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/module.ts @@ -4,10 +4,14 @@ import { Store } from "@ngrx/store"; import { ComponentsModule } from "src/components"; import { AngularMaterialModule } from "src/sharedModules"; import { atlasAppearance } from "src/state"; +import { StrictLocalModule } from "src/strictLocal"; +import { DialogModule } from "src/ui/dialogInfo/module"; import { UtilModule } from "src/util"; +import { SapiViewsUtilModule } from "../../util"; import { SapiViewsCoreParcellationParcellationChip } from "./chip/parcellation.chip.component"; import { FilterGroupedParcellationPipe } from "./filterGroupedParcellations.pipe"; import { FilterUnsupportedParcPipe } from "./filterUnsupportedParc.pipe"; +import { ParcellationDoiPipe } from "./parcellationDoi.pipe"; import { ParcellationIsBaseLayer } from "./parcellationIsBaseLayer.pipe"; import { ParcellationVisibilityService } from "./parcellationVis.service"; import { PreviewParcellationUrlPipe } from "./previewParcellationUrl.pipe"; @@ -20,6 +24,9 @@ import { SapiViewsCoreParcellationParcellationTile } from "./tile/parcellation.t ComponentsModule, AngularMaterialModule, UtilModule, + SapiViewsUtilModule, + DialogModule, + StrictLocalModule ], declarations: [ SapiViewsCoreParcellationParcellationTile, @@ -29,6 +36,7 @@ import { SapiViewsCoreParcellationParcellationTile } from "./tile/parcellation.t FilterGroupedParcellationPipe, FilterUnsupportedParcPipe, ParcellationIsBaseLayer, + ParcellationDoiPipe, ], exports: [ SapiViewsCoreParcellationParcellationTile, diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationDoi.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationDoi.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..8652365e1bb40d6565ccfd9d88e30eb94492c6d4 --- /dev/null +++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationDoi.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; + +@Pipe({ + name: 'parcellationDoiPipe', + pure: true +}) + +export class ParcellationDoiPipe implements PipeTransform { + public transform(parc: SapiParcellationModel): string[] { + const urls = (parc?.brainAtlasVersions || []).filter( + v => v.digitalIdentifier && v.digitalIdentifier['@type'] === 'https://openminds.ebrains.eu/core/DOI' + ).map( + v => v.digitalIdentifier['@id'] as string + ) + return Array.from(new Set(urls)) + } +} diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts index 7bd6f31f13409a290cd1adf7e9062eb04cd4e591..2c3c09a9d9b7307a7a1f8b18c193565137e9bd2f 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationIsBaseLayer.pipe.ts @@ -2,9 +2,31 @@ import { Pipe, PipeTransform } from "@angular/core"; import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; const baseLayerIds = [ + /** + * julich brain + */ "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290", "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-25", "minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579", + + /** + * allen mouse + */ + "minds/core/parcellationatlas/v1.0.0/05655b58-3b6f-49db-b285-64b5a0276f83", + "minds/core/parcellationatlas/v1.0.0/39a1384b-8413-4d27-af8d-22432225401f", + + /** + * waxholm + */ + "minds/core/parcellationatlas/v1.0.0/11017b35-7056-4593-baad-3934d211daba", + "minds/core/parcellationatlas/v1.0.0/2449a7f0-6dd0-4b5a-8f1e-aec0db03679d", + "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe", + "minds/core/parcellationatlas/v1.0.0/ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4", + + /** + * monkey + */ + "minds/core/parcellationatlas/v1.0.0/mebrains-tmp-id", ] @Pipe({ diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts index 5a7eb1fc32405cd1a4f9128cfda54c018eb45766..1d446f159af3a6cb1e52fc322e560a8eca6ad01b 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts @@ -3,15 +3,19 @@ import { SAPI } from "src/atlasComponents/sapi/sapi.service" import { SapiParcellationModel } from "src/atlasComponents/sapi/type" import { getTraverseFunctions } from "./parcellationVersion.pipe" -describe("parcellationVersion.pipe.ts", () => { +describe(`parcellationVersion.pipe.ts`, () => { describe("getTraverseFunctions", () => { let julichBrainParcellations: SapiParcellationModel[] = [] + let endpoint: string beforeAll(async () => { - const res = await fetch(`${SAPI.bsEndpoint}/atlases/${encodeURIComponent(IDS.ATLAES.HUMAN)}/parcellations`) + const bsEndPoint = await SAPI.BsEndpoint$.toPromise() + endpoint = bsEndPoint + const res = await fetch(`${bsEndPoint}/atlases/${encodeURIComponent(IDS.ATLAES.HUMAN)}/parcellations`) const arr: SapiParcellationModel[] = await res.json() julichBrainParcellations = arr.filter(it => /Julich-Brain Cytoarchitectonic Maps/.test(it.name)) }) it("> should be at least 3 parcellations", () => { + console.log(`testing against endpoint: ${endpoint}`) expect(julichBrainParcellations.length).toBeGreaterThanOrEqual(3) }) diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts index f56ae3fdb4de939dad8b5ac6285f10a81e7cda8b..27d4a53cc941a95164407b86f61825c01df1527f 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.component.ts @@ -1,9 +1,10 @@ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core"; -import { Observable } from "rxjs"; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges } from "@angular/core"; +import { BehaviorSubject, concat, Observable, of, timer } from "rxjs"; import { SapiParcellationModel } from "src/atlasComponents/sapi/type"; import { ParcellationVisibilityService } from "../parcellationVis.service"; import { ARIA_LABELS } from "common/constants" import { getTraverseFunctions } from "../parcellationVersion.pipe"; +import { mapTo, shareReplay, switchMap } from "rxjs/operators"; @Component({ selector: `sxplr-sapiviews-core-parcellation-smartchip`, @@ -37,7 +38,11 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges otherVersions: SapiParcellationModel[] - ngOnChanges() { + ngOnChanges(changes: SimpleChanges) { + const { parcellation } = changes + if (parcellation) { + this.onDismissClicked$.next(false) + } this.otherVersions = [] if (!this.parcellation) { return @@ -64,6 +69,16 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges } } + loadingParc$: Observable<SapiParcellationModel> = this.onSelectParcellation.pipe( + switchMap(parc => concat( + of(parc), + timer(5000).pipe( + mapTo(null) + ), + )), + shareReplay(1), + ) + parcellationVisibility$: Observable<boolean> = this.svc.visibility$ toggleParcellationVisibility(){ @@ -71,11 +86,19 @@ export class SapiViewsCoreParcellationParcellationSmartChip implements OnChanges } dismiss(){ + if (this.onDismissClicked$.value) return + this.onDismissClicked$.next(true) this.onDismiss.emit(this.parcellation) } selectParcellation(parc: SapiParcellationModel){ - if (parc === this.parcellation) return + if (this.trackByFn(parc) === this.trackByFn(this.parcellation)) return this.onSelectParcellation.emit(parc) } + + trackByFn(parc: SapiParcellationModel){ + return parc["@id"] + } + + onDismissClicked$ = new BehaviorSubject<boolean>(false) } diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f83fb1f49984f554b1a3fbcea0852d7f8005949b 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.style.css @@ -0,0 +1,34 @@ +.otherversion-wrapper +{ + position: relative; + overflow: hidden; + margin: 0.5rem; +} + +.otherversion-wrapper.loading > .spinner-container +{ + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + + display: flex; + align-items: center; +} + +.otherversion-wrapper.loading > .spinner-container > spinner-cmp +{ + margin: 0.5rem; +} + +.icons-container +{ + transform: scale(0.7); + margin-right: -1.5rem; +} + +.icons-container > * +{ + margin: auto 0.2rem; +} diff --git a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html index f792f5ce186c694e5b4d56f3335611c2ecc302b5..4dac117899e6a081d5d4309053c67d51ee9e714f 100644 --- a/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html +++ b/src/atlasComponents/sapiViews/core/parcellation/smartChip/parcellation.smartChip.template.html @@ -1,21 +1,54 @@ <mat-menu #otherParcMenu="matMenu" [hasBackdrop]="false" - class="sxplr-bg-none sxplr-of-x-hidden sxplr-box-shadow-none sxplr-mxw-80vw"> + class="parc-smart-chip-menu-panel sxplr-bg-none sxplr-of-x-hidden sxplr-box-shadow-none sxplr-mxw-80vw"> <div (iav-outsideClick)="menuTrigger.closeMenu()"> - <sxplr-sapiviews-core-parcellation-chip *ngFor="let parc of otherVersions" - [sxplr-sapiviews-core-parcellation-chip-parcellation]="parc" - [sxplr-sapiviews-core-parcellation-chip-color]="parcellation === parc ? 'primary' : 'default'" - (sxplr-sapiviews-core-parcellation-chip-onclick)="selectParcellation(parc)"> + <div *ngFor="let parc of otherVersions" + class="otherversion-wrapper" + [ngClass]="{ + 'loading': (loadingParc$ | async) === parc + }"> - </sxplr-sapiviews-core-parcellation-chip> + + <sxplr-sapiviews-core-parcellation-chip + [ngClass]="{ + 'sxplr-blink': (loadingParc$ | async) === parc + }" + [sxplr-sapiviews-core-parcellation-chip-parcellation]="parc" + [sxplr-sapiviews-core-parcellation-chip-color]="(parcellation | equality : parc : trackByFn) ? 'primary' : 'default'" + (sxplr-sapiviews-core-parcellation-chip-onclick)="selectParcellation(parc)"> + + <div class="sxplr-scale-70" + suffix + iav-stop="mousedown click"> + + <ng-template #otherParcDesc> + <ng-template [ngTemplateOutlet]="parcDescTmpl" + [ngTemplateOutletContext]="{ parcellation: parc }"> + </ng-template> + </ng-template> + + <button mat-mini-fab color="default" + [sxplr-dialog]="otherParcDesc" + [sxplr-dialog-size]="null"> + <i class="fas fa-info"></i> + </button> + </div> + </sxplr-sapiviews-core-parcellation-chip> + + <div class="spinner-container" *ngIf="(loadingParc$ | async) === parc"> + <spinner-cmp> + </spinner-cmp> + </div> + </div> </div> </mat-menu> <sxplr-sapiviews-core-parcellation-chip [ngClass]="{ - 'sxplr-muted': !(parcellationVisibility$ | async) + 'sxplr-muted': !(parcellationVisibility$ | async), + 'sxplr-blink': onDismissClicked$ | async }" class="sxplr-d-inline-block" [sxplr-sapiviews-core-parcellation-chip-parcellation]="parcellation" @@ -25,9 +58,25 @@ #menuTrigger="matMenuTrigger" > - <div prefix class="sxplr-scale-70"> - <button mat-mini-fab - [color]="(parcellationVisibility$ | async) ? 'primary' : 'default'" + <div class="icons-container" + suffix + iav-stop="mousedown click"> + + <ng-template #mainParcDesc> + <ng-template [ngTemplateOutlet]="parcDescTmpl" + [ngTemplateOutletContext]="{ parcellation: parcellation }"> + </ng-template> + </ng-template> + + <button mat-icon-button + color="default" + [sxplr-dialog]="mainParcDesc" + [sxplr-dialog-size]="null"> + <i class="fas fa-info"></i> + </button> + + <button mat-icon-button + color="default" [matTooltip]="ARIA_LABELS.TOGGLE_DELINEATION" iav-stop="mousedown click" [iav-key-listener]="[{'type': 'keydown', 'key': 'q', 'capture': true, 'target': 'document' }]" @@ -41,16 +90,47 @@ {{ ARIA_LABELS.TOGGLE_DELINEATION }} </span> </button> - </div> - <div *ngIf="!(parcellation | parcellationIsBaseLayer)" - class="sxplr-scale-70" - suffix> <button mat-mini-fab - color="primary" - iav-stop="mousedown click" + *ngIf="!(parcellation | parcellationIsBaseLayer)" + color="default" (click)="dismiss()"> - <i class="fas fa-times"></i> + + <spinner-cmp class="sxplr-w-100 sxplr-h-100" *ngIf="onDismissClicked$ | async; else defaultDismissIcon"></spinner-cmp> + <ng-template #defaultDismissIcon> + <i class="fas fa-times"></i> + </ng-template> + </button> </div> -</sxplr-sapiviews-core-parcellation-chip> \ No newline at end of file +</sxplr-sapiviews-core-parcellation-chip> + +<!-- parcellation description template --> + +<ng-template #parcDescTmpl let-parc="parcellation"> + <h1 mat-dialog-title> + {{ parc.name }} + </h1> + <div mat-dialog-content> + <markdown-dom + *ngIf="parc.brainAtlasVersions.length > 0 && parc.brainAtlasVersions[0].versionInnovation" + [markdown]="parc.brainAtlasVersions[0].versionInnovation"> + </markdown-dom> + </div> + + <mat-dialog-actions align="start"> + <a *ngFor="let url of parc | parcellationDoiPipe" + [href]="url" + sxplr-hide-when-local + target="_blank" + mat-raised-button + color="primary"> + <div class="fas fa-external-link-alt"></div> + <span> + Dataset Detail + </span> + </a> + + <button mat-button mat-dialog-close>Close</button> + </mat-dialog-actions> +</ng-template> diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts index f0e19a9bc9a82c83dc8e3501ce3f2b99ff51b7bd..60fd2425cc9b9f67008b8caa8c386dcf32614bd8 100644 --- a/src/atlasComponents/sapiViews/core/region/module.ts +++ b/src/atlasComponents/sapiViews/core/region/module.ts @@ -1,7 +1,9 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { MarkdownModule } from "src/components/markdown"; import { SpinnerModule } from "src/components/spinner"; import { AngularMaterialModule } from "src/sharedModules"; +import { StrictLocalModule } from "src/strictLocal"; import { SapiViewsFeaturesModule } from "../../features"; import { SapiViewsUtilModule } from "../../util/module"; import { SapiViewsCoreRegionRegionChip } from "./region/chip/region.chip.component"; @@ -17,6 +19,8 @@ import { SapiViewsCoreRegionRegionRich } from "./region/rich/region.rich.compone SapiViewsUtilModule, SapiViewsFeaturesModule, SpinnerModule, + MarkdownModule, + StrictLocalModule, ], declarations: [ SapiViewsCoreRegionRegionListItem, diff --git a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts index 39c1ccc4aa4429380aeae48ded57df5e68dfd3ac..96f374b267ced1fb700333654b7e62a39a9e311a 100644 --- a/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts +++ b/src/atlasComponents/sapiViews/core/region/region/region.base.directive.ts @@ -2,7 +2,7 @@ import { Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core import { SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"; import { rgbToHsl } from 'common/util' import { SAPI } from "src/atlasComponents/sapi/sapi.service"; -import { Subject } from "rxjs"; +import { BehaviorSubject, Subject } from "rxjs"; import { SAPIRegion } from "src/atlasComponents/sapi/core"; @Directive({ @@ -13,7 +13,8 @@ export class SapiViewsCoreRegionRegionBase { @Input('sxplr-sapiviews-core-region-detail-flag') shouldFetchDetail = false - public fetchInProgress = false + + public fetchInProgress$ = new BehaviorSubject<boolean>(false) @Input('sxplr-sapiviews-core-region-atlas') atlas: SapiAtlasModel @@ -37,7 +38,7 @@ export class SapiViewsCoreRegionRegionBase { this.setupRegionDarkmode() return } - this.fetchInProgress = true + this.fetchInProgress$.next(true) this._region = null this.fetchDetail(val) @@ -49,7 +50,7 @@ export class SapiViewsCoreRegionRegionBase { this._region = val }) .finally(() => { - this.fetchInProgress = false + this.fetchInProgress$.next(false) this.setupRegionDarkmode() }) } diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts index 79e4d693d5b23cd7a380f26c6cc8e5b922bba450..0d506febda44ca7366f7366a622403b06a488228 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.component.ts @@ -5,6 +5,7 @@ import { SapiViewsCoreRegionRegionBase } from "../region.base.directive"; import { ARIA_LABELS, CONST } from 'common/constants' import { SapiRegionalFeatureModel } from "src/atlasComponents/sapi"; import { SAPI } from "src/atlasComponents/sapi/sapi.service"; +import { environment } from "src/environments/environment"; @Component({ selector: 'sxplr-sapiviews-core-region-region-rich', @@ -17,7 +18,8 @@ import { SAPI } from "src/atlasComponents/sapi/sapi.service"; export class SapiViewsCoreRegionRegionRich extends SapiViewsCoreRegionRegionBase { - shouldFetchDetail = true + public environment = environment + public shouldFetchDetail = true public ARIA_LABELS = ARIA_LABELS public CONST = CONST diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html index 22ebc5640a5a7c9787f51d44e4cdfd9068558018..953ac5d04f7a1e89c439c33d952c99ce925f3d48 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html @@ -4,128 +4,145 @@ <ng-template [ngIf]="region"> -<mat-card class="mat-elevation-z4"> - <div - [style.backgroundColor]="regionRgbString" - class="vanishing-border" - [ngClass]="{ - 'darktheme': regionDarkmode === true, - 'lighttheme': regionDarkmode === false - }"> + <mat-card class="mat-elevation-z4"> + <div + [style.backgroundColor]="regionRgbString" + class="vanishing-border" + [ngClass]="{ + 'darktheme': regionDarkmode === true, + 'lighttheme': regionDarkmode === false + }"> - <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> - - <mat-card-title class="sxplr-custom-cmp text"> - {{ region.name }} - </mat-card-title> - - - <!-- subtitle on what it is --> - <mat-card-subtitle class="d-inline-flex align-items-center flex-wrap"> - <mat-icon fontSet="fas" fontIcon="fa-brain"></mat-icon> - <span> - Brain region - </span> - - <!-- origin datas format --> - - <mat-divider vertical="true" class="sxplr-pl-2 h-2rem"></mat-divider> - - <!-- position --> - <button mat-icon-button *ngIf="regionPosition" - (click)="navigateTo(regionPosition)" - [matTooltip]="ARIA_LABELS.GO_TO_REGION_CENTROID + ': ' + (regionPosition | numbers | addUnitAndJoin : 'mm')"> - <mat-icon fontSet="fas" fontIcon="fa-map-marked-alt"> - </mat-icon> - </button> - - <!-- explore doi --> - <a *ngFor="let doi of dois" - [href]="doi | parseDoi" - [matTooltip]="ARIA_LABELS.EXPLORE_DATASET_IN_KG" - target="_blank" - mat-icon-button> - <i class="fas fa-external-link-alt"></i> - </a> - - </mat-card-subtitle> - - </div> -</mat-card> - - -<!-- kg regional features list --> -<ng-template #kgRegionalFeatureList> - <div sxplr-sapiviews-core-region-regional-feature - [sxplr-sapiviews-core-region-atlas]="atlas" - [sxplr-sapiviews-core-region-template]="template" - [sxplr-sapiviews-core-region-parcellation]="parcellation" - [sxplr-sapiviews-core-region-region]="region" - #rfDir="sapiViewsRegionalFeature" - class="feature-list-container" - > - - <spinner-cmp *ngIf="rfDir.busy$ | async"></spinner-cmp> - - <sxplr-sapiviews-features-entry-list-item - *ngFor="let feat of rfDir.listOfFeatures$ | async" - [sxplr-sapiviews-features-entry-list-item-feature]="feat" - (click)="handleRegionalFeatureClicked(feat)"> - </sxplr-sapiviews-features-entry-list-item> - </div> - -</ng-template> + <ng-template [ngTemplateOutlet]="headerTmpl"></ng-template> + <mat-card-title class="sxplr-custom-cmp text"> + {{ region.name }} + </mat-card-title> + <!-- subtitle on what it is --> + <mat-card-subtitle class="d-inline-flex align-items-center flex-wrap"> + <mat-icon fontSet="fas" fontIcon="fa-brain"></mat-icon> + <span> + Brain region + </span> -<mat-accordion class="d-block mt-2"> + <!-- origin datas format --> + + <mat-divider vertical="true" class="sxplr-pl-2 h-2rem"></mat-divider> + + <!-- position --> + <button mat-icon-button *ngIf="regionPosition" + (click)="navigateTo(regionPosition)" + [matTooltip]="ARIA_LABELS.GO_TO_REGION_CENTROID + ': ' + (regionPosition | numbers | addUnitAndJoin : 'mm')"> + <mat-icon fontSet="fas" fontIcon="fa-map-marked-alt"> + </mat-icon> + </button> + + <!-- explore doi --> + <a *ngFor="let doi of dois" + [href]="doi | parseDoi" + sxplr-hide-when-local + [matTooltip]="ARIA_LABELS.EXPLORE_DATASET_IN_KG" + target="_blank" + mat-icon-button> + <i class="fas fa-external-link-alt"></i> + </a> + + </mat-card-subtitle> + + </div> + </mat-card> + + + <!-- kg regional features list --> + <ng-template #kgRegionalFeatureList> + <div sxplr-sapiviews-core-region-regional-feature + [sxplr-sapiviews-core-region-atlas]="atlas" + [sxplr-sapiviews-core-region-template]="template" + [sxplr-sapiviews-core-region-parcellation]="parcellation" + [sxplr-sapiviews-core-region-region]="region" + #rfDir="sapiViewsRegionalFeature" + class="feature-list-container" + > + + <spinner-cmp *ngIf="rfDir.busy$ | async"></spinner-cmp> + + <sxplr-sapiviews-features-entry-list-item + *ngFor="let feat of rfDir.listOfFeatures$ | async | orderFilterFeatures" + [sxplr-sapiviews-features-entry-list-item-feature]="feat" + (click)="handleRegionalFeatureClicked(feat)"> + </sxplr-sapiviews-features-entry-list-item> + </div> + + </ng-template> - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: CONST.REGIONAL_FEATURES, - iconClass: 'fas fa-database', - content: kgRegionalFeatureList, - desc: '', - iconTooltip: 'Regional Features', - iavNgIf: true - }"> - </ng-container> + <ng-template #regionDesc> + <markdown-dom class="sxplr-muted" [markdown]="region?.versionInnovation || 'No description provided.'"> + </markdown-dom> + </ng-template> -</mat-accordion> + <mat-accordion class="d-block mt-2"> -<mat-accordion class="d-block mt-2"> + <!-- desc --> + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: CONST.DESCRIPTION, + iconClass: 'fas fa-info', + content: regionDesc, + desc: '', + iconTooltip: 'Description', + iavNgIf: !!region?.versionInnovation + }"> - <!-- connectivity --> - <ng-template #sxplrSapiviewsFeaturesConnectivityBrowser> - <sxplr-sapiviews-features-connectivity-browser class="pe-all flex-shrink-1" - [region]="region" - [types]="hasConnectivityDirective.availableModalities" - [defaultProfile]="hasConnectivityDirective.defaultProfile" - [sxplr-sapiviews-features-connectivity-browser-atlas]="atlas" - [sxplr-sapiviews-features-connectivity-browser-parcellation]="parcellation" - [accordionExpanded]="expandedPanel === CONST.CONNECTIVITY" - > - </sxplr-sapiviews-features-connectivity-browser> - </ng-template> + </ng-container> + + <!-- only show dynamic data when strict-local is set to false --> + <ng-template [ngIf]="!environment.STRICT_LOCAL"> + + <!-- feature list --> + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: CONST.REGIONAL_FEATURES, + iconClass: 'fas fa-database', + content: kgRegionalFeatureList, + desc: '', + iconTooltip: 'Regional Features', + iavNgIf: true + }"> + </ng-container> + + <!-- connectivity --> + <ng-template #sxplrSapiviewsFeaturesConnectivityBrowser> + <sxplr-sapiviews-features-connectivity-browser + class="pe-all flex-shrink-1" + [region]="region" + [types]="hasConnectivityDirective.availableModalities" + [defaultProfile]="hasConnectivityDirective.defaultProfile" + [sxplr-sapiviews-features-connectivity-browser-atlas]="atlas" + [sxplr-sapiviews-features-connectivity-browser-parcellation]="parcellation" + [accordionExpanded]="expandedPanel === CONST.CONNECTIVITY" + > + </sxplr-sapiviews-features-connectivity-browser> + </ng-template> + + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: CONST.CONNECTIVITY, + iconClass: 'fab fa-connectdevelop', + content: sxplrSapiviewsFeaturesConnectivityBrowser, + desc: hasConnectivityDirective.connectivityNumber, + iconTooltip: hasConnectivityDirective.connectivityNumber + 'Connections', + iavNgIf: hasConnectivityDirective.hasConnectivity + }"> + </ng-container> + + <div sxplr-sapiviews-features-connectivity-check + [sxplr-sapiviews-features-connectivity-check-atlas]="atlas" + [sxplr-sapiviews-features-connectivity-check-parcellation]="parcellation" + [region]="region" + #hasConnectivityDirective="hasConnectivityDirective"> + </div> + </ng-template> - <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { - title: CONST.CONNECTIVITY, - iconClass: 'fab fa-connectdevelop', - content: sxplrSapiviewsFeaturesConnectivityBrowser, - desc: hasConnectivityDirective.connectivityNumber, - iconTooltip: hasConnectivityDirective.connectivityNumber + 'Connections', - iavNgIf: hasConnectivityDirective.hasConnectivity - }"> - </ng-container> - - <div sxplr-sapiviews-features-connectivity-check - [sxplr-sapiviews-features-connectivity-check-atlas]="atlas" - [sxplr-sapiviews-features-connectivity-check-parcellation]="parcellation" - [region]="region" - #hasConnectivityDirective="hasConnectivityDirective"> - </div> - -</mat-accordion> + </mat-accordion> </ng-template> diff --git a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.stories.ts b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.stories.ts index 8dbfce6e38bba1201cdd7d94adc310c0381cf1de..5dca7cbc394a3baa2360349fb9aafa7c08d6c5bc 100644 --- a/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.stories.ts +++ b/src/atlasComponents/sapiViews/core/rich/regionsHierarchy/regionsHierarchy.stories.ts @@ -117,10 +117,16 @@ const asyncLoader = async () => { const atlasDetail = await getAtlas(atlasId[species]) regionsDict[species] = {} - for (const parc of atlasDetail.parcellations) { - const parcDetail = await getParc(atlasDetail['@id'], parc['@id']) - regionsDict[species][parcDetail.name] = await getParcRegions(atlasDetail['@id'], parc['@id'], atlasDetail.spaces[0]["@id"] ) - } + await Promise.all( + atlasDetail.parcellations.map(async parc => { + try { + const parcDetail = await getParc(atlasDetail['@id'], parc['@id']) + regionsDict[species][parcDetail.name] = await getParcRegions(atlasDetail['@id'], parc['@id'], atlasDetail.spaces[0]["@id"] ) + } catch (e) { + console.warn(`fetching region detail for ${parc["@id"]} failed... Skipping...`) + } + }) + ) } return { diff --git a/src/atlasComponents/sapiViews/core/space/index.ts b/src/atlasComponents/sapiViews/core/space/index.ts index 46f783b69e03bdae2ef01144ba731e0b264c1c12..26c7eed07b1e454d4eac3bcbdf0c77bb9fe4f162 100644 --- a/src/atlasComponents/sapiViews/core/space/index.ts +++ b/src/atlasComponents/sapiViews/core/space/index.ts @@ -1 +1,4 @@ -export { SapiViewsCoreSpaceModule } from "./module" \ No newline at end of file +export { SapiViewsCoreSpaceModule } from "./module" +export { + SapiViewsCoreSpaceBoundingBox +} from "./boundingBox.directive" \ No newline at end of file diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts index 3889c4c3f20621280a66f92f3e37f2a4521414d0..b1efcd694501bd33796a961449fbb3fec244b28c 100644 --- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.spec.ts @@ -6,7 +6,6 @@ import {CUSTOM_ELEMENTS_SCHEMA, Directive, Input} from "@angular/core"; import {provideMockActions} from "@ngrx/effects/testing"; import {MockStore, provideMockStore} from "@ngrx/store/testing"; import {Observable, of} from "rxjs"; -import {BS_ENDPOINT} from "src/util/constants"; import {SAPI} from "src/atlasComponents/sapi"; import {AngularMaterialModule} from "src/sharedModules"; @@ -66,10 +65,6 @@ describe('ConnectivityComponent', () => { providers: [ provideMockActions(() => actions$), provideMockStore(), - { - provide: BS_ENDPOINT, - useValue: MOCK_BS_ENDPOINT - }, { provide: SAPI, useValue: { diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts index 23d89f519a537c5a296ba90bd1dfd771bc17a3d5..2399e2b668ab21186da82384bd0f2138ec095ff2 100644 --- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts @@ -216,10 +216,8 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { // ToDo this temporary fix is for the bug existing on siibra api https://github.com/FZJ-INM1-BDA/siibra-api/issues/100 private fixDatasetFormat = (ds) => ds.name.includes('{')? ({ ...ds, - name: ds.name.substr(0, ds.name.indexOf('{')), - dataset: JSON.parse(ds.name.substring(ds.name.indexOf('{')).replace(/'/g, '"')) + ...JSON.parse(ds.name.substring(ds.name.indexOf('{')).replace(/'/g, '"')) }) : ds - fetchConnectivity() { this.sapi.getParcellation(this.atlas["@id"], this.parcellation["@id"]).getFeatureInstance(this.selectedDataset['@id']) @@ -232,7 +230,7 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { this.fetching = false }) } - + setMatrixData(data) { const matrixData = data as SapiParcellationFeatureMatrixModel this.regionIndexInMatrix = (matrixData.columns as Array<string>).findIndex(md => md === this.regionName) @@ -248,7 +246,7 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { .then(matrix => { const regionProfile = matrix.rawArray[this.regionIndexInMatrix] - const maxStrength = Math.max(...regionProfile) + const maxStrength = Math.max(...regionProfile) this.logChecked = maxStrength > 1 this.logDisabled = maxStrength <= 1 @@ -264,7 +262,7 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { }) } - + changeLog(checked: boolean) { this.logChecked = checked this.connectedAreas.next(this.formatConnections(this.pureConnections)) @@ -272,13 +270,18 @@ export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { this.setCustomLayer() } - //ToDo navigateRegion action does not work any more + //ToDo bestViewPoint is null for the most cases navigateToRegion(region: SapiRegionModel) { - this.store$.dispatch( - atlasSelection.actions.navigateToRegion({ - region - }) - ) + const regionCentroid = this.region.hasAnnotation?.bestViewPoint?.coordinates + if (regionCentroid) + this.store$.dispatch( + atlasSelection.actions.navigateTo({ + navigation: { + position: regionCentroid.map(v => v.value*1e6), + }, + animation: true + }) + ) } getRegionWithName(region: string) { diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html index 724f131a5ebcb37af35bbee1a3c30f74b57d345a..fae388859e866bb9de1439337ef741d41ede85fa 100644 --- a/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityBrowser/connectivityBrowser.template.html @@ -62,10 +62,11 @@ theme="dark"> </hbp-connectivity-matrix-row> <div *ngIf="noConnectivityForRegion">No connectivity for the region.</div> + <full-connectivity-grid #fullConnectivityGrid [matrix]="matrixString" - [datasetName]="selectedDataset?.dataset?.name" - [datasetDescription]="selectedDataset?.dataset?.description" + [datasetName]="selectedDataset?.name" + [datasetDescription]="selectedDataset?.description" only-export="true"> </full-connectivity-grid> diff --git a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts index 3b915ff248a21b0cf19d3dc5fbac8131b2d21643..8008affe34a6b783fd1b93ed14ee527279e36d04 100644 --- a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts +++ b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; import { SapiFeatureModel } from "src/atlasComponents/sapi"; import { CleanedIeegDataset, CLEANED_IEEG_DATASET_TYPE, SapiDatasetModel, SapiParcellationFeatureMatrixModel, SapiRegionalFeatureReceptorModel, SapiSerializationErrorModel, SapiVOIDataResponse, SxplrCleanedFeatureModel } from "src/atlasComponents/sapi/type"; @@ -7,7 +7,8 @@ import { CleanedIeegDataset, CLEANED_IEEG_DATASET_TYPE, SapiDatasetModel, SapiPa templateUrl: `./entryListItem.template.html`, styleUrls: [ `./entryListItem.style.css` - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SapiViewsFeaturesEntryListItem{ diff --git a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html index 76f584328b3e46b5a240defb44e98e7cc2abe4a2..fc1ac7920b3599f52aafb071417eb6bb41c0d6e1 100644 --- a/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html +++ b/src/atlasComponents/sapiViews/features/entryListItem/entryListItem.template.html @@ -3,7 +3,7 @@ <mat-chip-list *ngIf="feature | featureBadgeFlag" - class="sxplr-scale-80 transform-origin-left-center"> + class="sxplr-scale-80 transform-origin-left-center sxplr-pe-none"> <mat-chip [color]="feature | featureBadgeColour" selected> diff --git a/src/atlasComponents/sapiViews/features/index.ts b/src/atlasComponents/sapiViews/features/index.ts index 19e30dcb1873aa37868be5e8944eaad3a9b0ce05..89e1fafbad8108576c708d401326b2d5f4137f6d 100644 --- a/src/atlasComponents/sapiViews/features/index.ts +++ b/src/atlasComponents/sapiViews/features/index.ts @@ -1,3 +1,7 @@ export { SapiViewsFeaturesModule -} from "./module" \ No newline at end of file +} from "./module" + +export { + SapiViewsFeaturesVoiQuery +} from "./voi" diff --git a/src/atlasComponents/sapiViews/features/module.ts b/src/atlasComponents/sapiViews/features/module.ts index cbf7aeacaeccaf721bb6ca55108b01366f3fa685..3df348c72f79be4b53c3050e30627c896e8b93f2 100644 --- a/src/atlasComponents/sapiViews/features/module.ts +++ b/src/atlasComponents/sapiViews/features/module.ts @@ -11,6 +11,7 @@ import * as ieeg from "./ieeg" import * as receptor from "./receptors" import {SapiViewsFeatureConnectivityModule} from "src/atlasComponents/sapiViews/features/connectivity"; import * as voi from "./voi" +import { OrderFilterFeaturesPipe } from "./orderFilterFeatureList.pipe" const { SxplrSapiViewsFeaturesIeegModule @@ -35,6 +36,7 @@ const { SapiViewsFeaturesVoiModule } = voi FeatureBadgeColourPipe, FeatureBadgeFlagPipe, SapiViewsFeaturesEntryListItem, + OrderFilterFeaturesPipe, ], providers: [ { @@ -48,6 +50,7 @@ const { SapiViewsFeaturesVoiModule } = voi SapiViewsFeaturesEntryListItem, SapiViewsFeaturesVoiModule, SapiViewsFeatureConnectivityModule, + OrderFilterFeaturesPipe, ] }) export class SapiViewsFeaturesModule{} diff --git a/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts b/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..382862b0300f4bdaffadce20aeb4e28274a8bef6 --- /dev/null +++ b/src/atlasComponents/sapiViews/features/orderFilterFeatureList.pipe.ts @@ -0,0 +1,37 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { CLEANED_IEEG_DATASET_TYPE, SapiFeatureModel, SxplrCleanedFeatureModel } from "src/atlasComponents/sapi/type"; +import { environment } from "src/environments/environment" + +type PipableFeatureType = SapiFeatureModel | SxplrCleanedFeatureModel + +type ArrayOperation<T extends boolean | number> = (input: PipableFeatureType) => T + +const FILTER_FN: ArrayOperation<boolean> = feature => { + return feature["@type"] !== "siibra/features/cells" +} + +const ORDER_LIST: ArrayOperation<number> = feature => { + if (feature["@type"] === "siibra/features/receptor") return -4 + if (feature["@type"] === CLEANED_IEEG_DATASET_TYPE) return -3 + if (feature['@type'] === "https://openminds.ebrains.eu/core/DatasetVersion") return 2 + return 0 +} + +@Pipe({ + name: 'orderFilterFeatures', + pure: true +}) + +export class OrderFilterFeaturesPipe implements PipeTransform{ + public transform(inputFeatures: PipableFeatureType[]): PipableFeatureType[] { + return inputFeatures + .filter(f => { + /** + * if experimental flag is set, do not filter out anything + */ + if (environment.EXPERIMENTAL_FEATURE_FLAG) return true + return FILTER_FN(f) + }) + .sort((a, b) => ORDER_LIST(a) - ORDER_LIST(b)) + } +} diff --git a/src/atlasComponents/sapiViews/features/receptors/module.ts b/src/atlasComponents/sapiViews/features/receptors/module.ts index d5bd04dc6b11f71c1646b4c8b2892c6ddf372f6b..34f29d7f07be5b5df7ddfaad65d9f2c6423fbb04 100644 --- a/src/atlasComponents/sapiViews/features/receptors/module.ts +++ b/src/atlasComponents/sapiViews/features/receptors/module.ts @@ -32,21 +32,6 @@ import { Profile } from "./profile/profile.component" Profile, Entry, ], - providers: [{ - provide: APP_INITIALIZER, - multi: true, - useFactory: (appendScriptFn: (url: string) => Promise<any>) => { - - const libraries = [ - 'https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js', - 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.1.2/es5/tex-svg.js' - ] - return () => Promise.all(libraries.map(appendScriptFn)) - }, - deps: [ - APPEND_SCRIPT_TOKEN - ] - }], schemas: [ CUSTOM_ELEMENTS_SCHEMA, ] diff --git a/src/atlasComponents/sapiViews/util/equality.pipe.ts b/src/atlasComponents/sapiViews/util/equality.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..8251d641fed51ac99384c6470af5623e0c37451c --- /dev/null +++ b/src/atlasComponents/sapiViews/util/equality.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe } from "@angular/core"; + +type TTrackBy<T, O> = (input: T) => O + +const defaultTrackBy: TTrackBy<unknown, unknown> = i => i + +@Pipe({ + name: 'equality', + pure: true +}) + +export class EqualityPipe<T>{ + public transform(c1: T, c2: T, trackBy: TTrackBy<T, unknown> = defaultTrackBy): boolean { + return trackBy(c1) === trackBy(c2) + } +} diff --git a/src/atlasComponents/sapiViews/util/module.ts b/src/atlasComponents/sapiViews/util/module.ts index 4a9eddaf8ef6cb42a5b0b5b1bec270fac766f82b..53a3a88c549821dfe9a7ee654c93dac7747cd368 100644 --- a/src/atlasComponents/sapiViews/util/module.ts +++ b/src/atlasComponents/sapiViews/util/module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; import { AddUnitAndJoin } from "./addUnitAndJoin.pipe"; +import { EqualityPipe } from "./equality.pipe"; import { IncludesPipe } from "./includes.pipe"; import { NumbersPipe } from "./numbers.pipe"; import { ParcellationSupportedInCurrentSpace } from "./parcellationSupportedInCurrentSpace.pipe"; @@ -9,6 +10,7 @@ import { SpaceSupportedInCurrentParcellationPipe } from "./spaceSupportedInCurre @NgModule({ declarations: [ + EqualityPipe, ParseDoiPipe, NumbersPipe, AddUnitAndJoin, @@ -18,6 +20,7 @@ import { SpaceSupportedInCurrentParcellationPipe } from "./spaceSupportedInCurre SpaceSupportedInCurrentParcellationPipe, ], exports: [ + EqualityPipe, ParseDoiPipe, NumbersPipe, AddUnitAndJoin, diff --git a/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts index f16e6a0a82d991ca8955e84f9ed4a15fe14e2ab4..42eec193e93fcd29dcf576c45135bf0b0f892eef 100644 --- a/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts +++ b/src/atlasComponents/sapiViews/util/parcellationSupportedInSpace.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from "@angular/core"; -import { Observable, of } from "rxjs"; +import { NEVER, Observable, of } from "rxjs"; import { map } from "rxjs/operators"; import { SAPIParcellation } from "src/atlasComponents/sapi/core"; import { SAPI } from "src/atlasComponents/sapi/sapi.service"; @@ -29,6 +29,7 @@ export class ParcellationSupportedInSpacePipe implements PipeTransform{ constructor(private sapi: SAPI){} public transform(parc: SapiParcellationModel|string, tmpl: SapiSpaceModel|string): Observable<boolean> { + if (!parc) return NEVER const parcId = typeof parc === "string" ? parc : parc["@id"] diff --git a/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts b/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts index fb6500281577a0791f6c82b7d53ce6b7aef636a6..d339398c0a55865d2a4fce6e2feb9246e4a5606b 100644 --- a/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts +++ b/src/atlasComponents/userAnnotations/filterAnnotationBySpace.pipe.ts @@ -14,7 +14,7 @@ export class FilterAnnotationsBySpace implements PipeTransform{ public transform(annotations: IAnnotationGeometry[], space: { '@id': string }, opts?: TOpts): IAnnotationGeometry[]{ const { reverse = false } = opts || {} return reverse - ? annotations.filter(ann => ann.space["@id"] !== space["@id"]) - : annotations.filter(ann => ann.space["@id"] === space["@id"]) + ? annotations.filter(ann => ann.space?.["@id"] !== space?.["@id"]) + : annotations.filter(ann => ann.space?.["@id"] === space?.["@id"]) } -} \ No newline at end of file +} diff --git a/src/atlasViewer/atlasViewer.workerService.service.ts b/src/atlasViewer/atlasViewer.workerService.service.ts index 9a17115534cae61a899a29c7d32bc4602599fd19..67a422714822d25e47fdb55b345116cda9db8eba 100644 --- a/src/atlasViewer/atlasViewer.workerService.service.ts +++ b/src/atlasViewer/atlasViewer.workerService.service.ts @@ -3,9 +3,6 @@ import { fromEvent } from "rxjs"; import { filter, take } from "rxjs/operators"; import { getUuid } from "src/util/fn"; -// worker is now exported in angular.json file -export const worker = new Worker('worker.js') - interface IWorkerMessage { method: string param: any @@ -17,7 +14,11 @@ interface IWorkerMessage { }) export class AtlasWorkerService { - public worker = worker + private worker: Worker + + constructor(){ + this.worker = new Worker('worker.js') + } async sendMessage(_data: IWorkerMessage){ diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index f4d5c96275db63b6e84e76a7223c597139fba508..129a0fe9e32d74a76fcc365fdc33a48e81604afc 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -4,7 +4,7 @@ export const environment = { VERSION: 'unknown version', PRODUCTION: true, BACKEND_URL: null, - BS_REST_URL: 'https://siibra-api-latest.apps-dev.hbp.eu/v2_0', + SIIBRA_API_ENDPOINTS: 'https://siibra-api-stable.apps.hbp.eu/v2_0,https://siibra-api-stable.apps.jsc.hbp.eu/v2_0,https://siibra-api-stable-ns.apps.hbp.eu/v2_0', SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu', MATOMO_URL: null, MATOMO_ID: null, @@ -17,4 +17,6 @@ export const environment = { // experimental feature flag EXPERIMENTAL_FEATURE_FLAG: false, + + ENABLE_LEAP_MOTION: false, } diff --git a/src/environments/parseEnv.js b/src/environments/parseEnv.js index b05909d596c85cc1875650602cafb8a5fa2fba3a..a9dd24424e3eb5ad2ef58b2396ac793461d8dd78 100644 --- a/src/environments/parseEnv.js +++ b/src/environments/parseEnv.js @@ -2,36 +2,69 @@ const fs = require('fs') const path = require('path') const { promisify } = require('util') const asyncWrite = promisify(fs.writeFile) +const asyncReadFile = promisify(fs.readFile) +const process = require("process") +const { exec } = require("child_process") + + +const getGitHead = () => new Promise((rs, rj) => { + exec(`git rev-parse --short HEAD`, (err, stdout, stderr) => { + if (err) return rj(err) + if (stderr) return rj(stderr) + rs(stdout) + }) +}) + +const getVersion = async () => { + const content = await asyncReadFile("./package.json", "utf-8") + const { version } = JSON.parse(content) + return version +} const main = async () => { - const pathToEnvFile = path.join(__dirname, './environment.prod.ts') + const target = process.argv[2] || './environment.prod.ts' + const pathToEnvFile = path.join(__dirname, target) const { BACKEND_URL, STRICT_LOCAL, MATOMO_URL, MATOMO_ID, - BS_REST_URL, - VERSION, - GIT_HASH = 'unknown hash', - EXPERIMENTAL_FEATURE_FLAG + SIIBRA_API_ENDPOINTS, + EXPERIMENTAL_FEATURE_FLAG, + ENABLE_LEAP_MOTION, } = process.env + const version = JSON.stringify( + await (async () => { + try { + return await getVersion() + } catch (e) { + return "unknown version" + } + })() + ) + const gitHash = JSON.stringify( + await (async () => { + try { + return await getGitHead() + } catch (e) { + return "unknown git hash" + } + })() + ) + console.log(`[parseEnv.js] parse envvar:`, { BACKEND_URL, STRICT_LOCAL, MATOMO_URL, MATOMO_ID, - BS_REST_URL, - VERSION, - GIT_HASH, + SIIBRA_API_ENDPOINTS, EXPERIMENTAL_FEATURE_FLAG, + ENABLE_LEAP_MOTION, + + VERSION: version, + GIT_HASH: gitHash, }) - const version = JSON.stringify( - VERSION || 'unknown version' - ) - const gitHash = JSON.stringify( - GIT_HASH || 'unknown hash' - ) const outputTxt = ` import { environment as commonEnv } from './environment.common' @@ -39,12 +72,13 @@ export const environment = { ...commonEnv, GIT_HASH: ${gitHash}, VERSION: ${version}, - BS_REST_URL: ${JSON.stringify(BS_REST_URL)}, + SIIBRA_API_ENDPOINTS: ${JSON.stringify(SIIBRA_API_ENDPOINTS)}, BACKEND_URL: ${JSON.stringify(BACKEND_URL)}, - STRICT_LOCAL: ${JSON.stringify(STRICT_LOCAL)}, + STRICT_LOCAL: ${STRICT_LOCAL}, MATOMO_URL: ${JSON.stringify(MATOMO_URL)}, MATOMO_ID: ${JSON.stringify(MATOMO_ID)}, - EXPERIMENTAL_FEATURE_FLAG: ${EXPERIMENTAL_FEATURE_FLAG} + EXPERIMENTAL_FEATURE_FLAG: ${EXPERIMENTAL_FEATURE_FLAG}, + ENABLE_LEAP_MOTION: ${ENABLE_LEAP_MOTION} } ` await asyncWrite(pathToEnvFile, outputTxt, 'utf-8') diff --git a/src/extra_styles.css b/src/extra_styles.css index a85cfbe825e0d260d99818e059a0927de8ce922a..643ab1e507c31fc6e833ca777748a16714e83982 100644 --- a/src/extra_styles.css +++ b/src/extra_styles.css @@ -390,11 +390,6 @@ markdown-dom p height: 0px; } -.pe-none -{ - pointer-events: none!important; -} - .h-2rem { height: 2rem!important; @@ -876,3 +871,8 @@ how-to-cite img { width: 100%; } + +.mat-menu-panel.parc-smart-chip-menu-panel +{ + max-width: 100vw; +} diff --git a/src/index.html b/src/index.html index a72f7d04781cae91eff5c233709a8059bab8e08d..763f059da834646019a8679113757031d3383804 100644 --- a/src/index.html +++ b/src/index.html @@ -16,6 +16,8 @@ <script src="https://unpkg.com/three-surfer@0.0.11/dist/bundle.js" defer></script> <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.6/dist/ng-layer-tune/ng-layer-tune.esm.js"></script> <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" ></script> + <script defer src="https://unpkg.com/mathjax@3.1.2/es5/tex-svg.js"></script> + <script defer src="https://unpkg.com/d3@6.2.0/dist/d3.min.js"></script> <title>Siibra Explorer</title> </head> <body> diff --git a/src/main.module.ts b/src/main.module.ts index 7d26a8ee52400fa2acd8e9cbd0a670ddd06c5670..da570799012cb86e814b3760f110174c943f8f36 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -33,7 +33,6 @@ import { CookieModule } from './ui/cookieAgreement/module'; import { KgTosModule } from './ui/kgtos/module'; import { AtlasViewerRouterModule } from './routerModule'; import { MessagingGlue } from './messagingGlue'; -import { BS_ENDPOINT } from './util/constants'; import { QuickTourModule } from './ui/quickTour'; import { of } from 'rxjs'; import { CANCELLABLE_DIALOG, CANCELLABLE_DIALOG_OPTS } from './util/interfaces'; @@ -163,10 +162,6 @@ import { CONST } from "common/constants" provide: WINDOW_MESSAGING_HANDLER_TOKEN, useClass: MessagingGlue }, - { - provide: BS_ENDPOINT, - useValue: (environment.BS_REST_URL || `https://siibra-api-stable.apps.hbp.eu/v1_0`).replace(/\/$/, '') - }, { provide: DARKTHEME, useFactory: (store: Store) => store.pipe( diff --git a/src/overwrite.scss b/src/overwrite.scss index a6b3e9f55255d3080d58a94e7be6aedce47fc890..86f98096cead1a68e768e98ebd77aa084859fdf2 100644 --- a/src/overwrite.scss +++ b/src/overwrite.scss @@ -264,4 +264,28 @@ $flex-directions: row,column; .#{$nsp}-flex-static { flex: 0 0 auto; -} \ No newline at end of file +} + +.#{$nsp}-blink +{ + animation: blink 500ms ease-in-out infinite alternate; +} + +@keyframes blink { + 0% { + opacity: 0.8; + } + 100% { + opacity: 0.5; + } +} + +a[mat-raised-button] +{ + text-decoration: none; +} + +.#{$nsp}-pe-none +{ + pointer-events: none!important; +} diff --git a/src/plugin/const.ts b/src/plugin/const.ts index 68104badfa030957d049a7acdfb58286fd254680..474bc36bf2cc119f845074735cc3073368db6a13 100644 --- a/src/plugin/const.ts +++ b/src/plugin/const.ts @@ -1,3 +1,5 @@ +import { InjectionToken } from "@angular/core" + const PLUGIN_SRC_KEY = "x-plugin-portal-src" export function setPluginSrc(src: string, record: Record<string, unknown> = {}){ @@ -10,3 +12,5 @@ export function setPluginSrc(src: string, record: Record<string, unknown> = {}){ export function getPluginSrc(record: Record<string, string> = {}){ return record[PLUGIN_SRC_KEY] } + +export const SET_PLUGIN_NAME = new InjectionToken('SET_PLUGIN_NAME') diff --git a/src/plugin/pluginPortal/pluginPortal.component.ts b/src/plugin/pluginPortal/pluginPortal.component.ts index 7d5b4d4214babf9c9af7ceda85e6e2e205530851..58bd58993f3c2423ed278fee3c64b330b78b4af2 100644 --- a/src/plugin/pluginPortal/pluginPortal.component.ts +++ b/src/plugin/pluginPortal/pluginPortal.component.ts @@ -5,8 +5,7 @@ import { BoothVisitor, JRPCRequest, JRPCSuccessResp, ListenerChannel } from "src import { ApiBoothEvents, ApiService, BroadCastingApiEvents, HeartbeatEvents, namespace } from "src/api/service"; import { getUuid } from "src/util/fn"; import { WIDGET_PORTAL_TOKEN } from "src/widget/constants"; -import { getPluginSrc } from "../const"; -import { PluginService } from "../service"; +import { getPluginSrc, SET_PLUGIN_NAME } from "../const"; @Component({ selector: 'sxplr-plugin-portal', @@ -44,8 +43,8 @@ export class PluginPortal implements AfterViewInit, OnDestroy, ListenerChannel{ constructor( private apiService: ApiService, - private pluginSvc: PluginService, public vcr: ViewContainerRef, + @Optional() @Inject(SET_PLUGIN_NAME) private setPluginName: (inst: unknown, pluginName: string) => void, @Optional() @Inject(WIDGET_PORTAL_TOKEN) portalData: Record<string, string> ){ if (portalData){ @@ -91,7 +90,7 @@ export class PluginPortal implements AfterViewInit, OnDestroy, ListenerChannel{ const data = event.data as JRPCSuccessResp<HeartbeatEvents['init']['response']> this.srcName = data.result.name || 'Untitled Pluging' - this.pluginSvc.setPluginName(this, this.srcName) + this.setPluginName(this, this.srcName) while (this.handshakeSub.length > 0) this.handshakeSub.pop().unsubscribe() diff --git a/src/plugin/request.md b/src/plugin/request.md index 487c1344f3d785978a99eb61f1ec94ca6b31904e..47daf4525b3873b10f93a87d75464e0e8cb97f13 100644 --- a/src/plugin/request.md +++ b/src/plugin/request.md @@ -9,7 +9,7 @@ Be it request the user to select a region, a point, navigate to a specific locat ```javascript let parentWindow -window.addEventListener('message', ev => { +window.addEventListener('message', msg => { const { source, data, origin } = msg const { id, method, params, result, error } = data diff --git a/src/plugin/service.ts b/src/plugin/service.ts index 4f5a5e74f5a6bfae42a6fd445b68fcd3c115e1f2..e4fe49a725535ac36d54daf87b4f79a0aae3f698 100644 --- a/src/plugin/service.ts +++ b/src/plugin/service.ts @@ -3,9 +3,10 @@ import { Injectable, Injector, NgZone } from "@angular/core"; import { WIDGET_PORTAL_TOKEN } from "src/widget/constants"; import { WidgetService } from "src/widget/service"; import { WidgetPortal } from "src/widget/widgetPortal/widgetPortal.component"; -import { setPluginSrc } from "./const"; +import { setPluginSrc, SET_PLUGIN_NAME } from "./const"; import { PluginPortal } from "./pluginPortal/pluginPortal.component"; import { environment } from "src/environments/environment" +import { startWith } from "rxjs/operators"; @Injectable({ providedIn: 'root' @@ -25,7 +26,9 @@ export class PluginService { 'siibra-explorer': true name: string iframeUrl: string - }[]>(`${environment.BACKEND_URL || ''}plugins/manifests`) + }[]>(`${environment.BACKEND_URL || ''}plugins/manifests`).pipe( + startWith([]) + ) async launchPlugin(htmlSrc: string){ if (this.loadedPlugins.includes(htmlSrc)) return @@ -33,6 +36,9 @@ export class PluginService { providers: [{ provide: WIDGET_PORTAL_TOKEN, useValue: setPluginSrc(htmlSrc, {}) + }, { + provide: SET_PLUGIN_NAME, + useValue: (inst: PluginPortal, pluginName: string) => this.setPluginName(inst, pluginName) }], parent: this.injector }) diff --git a/src/plugin_examples/README.md b/src/plugin_examples/README.md deleted file mode 100644 index 7f967539c0997992f1dae6e391a72e084d5a33a5..0000000000000000000000000000000000000000 --- a/src/plugin_examples/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# Plugin README - -A plugin needs to contain three files. -- Manifest JSON -- template HTML -- script JS - - -These files need to be served by GET requests over HTTP with appropriate CORS header. - ---- - -## Manifest JSON - -The manifest JSON file describes the metadata associated with the plugin. - -```json -{ - "name":"fzj.xg.helloWorld", - "displayName": "Hello World - my first plugin", - "templateURL":"http://LINK-TO-YOUR-PLUGIN-TEMPLATE/template.html", - "scriptURL":"http://LINK-TO-YOUR-PLUGIN-SCRIPT/script.js", - "initState":{ - "key1": "value1", - "key2" : { - "nestedKey1" : "nestedValue1" - } - }, - "initStateUrl": "http://LINK-TO-PLUGIN-STATE", - "persistency": false, - - "description": "Human readable description of the plugin.", - "desc": "Same as description. If both present, description takes more priority.", - "homepage": "https://HOMEPAGE-URL-TO-YOUR-PLUGIN/doc.html", - "authors": "Author <author@example.com>, Author2 <author2@example.org>" -} -``` -*NB* -- Plugin name must be unique globally. To prevent plugin name clashing, please adhere to the convention of naming your package **AFFILIATION.AUTHORNAME.PACKAGENAME\[.VERSION\]**. -- the `initState` object and `initStateUrl` will be available prior to the evaluation of `script.js`, and will populate the objects `interactiveViewer.pluginControl[MANIFEST.name].initState` and `interactiveViewer.pluginControl[MANIFEST.name].initStateUrl` respectively. - ---- - -## Template HTML - -The template HTML file describes the HTML view that will be rendered in the widget. - - -```html -<form> - <div class = "input-group"> - <span class = "input-group-addon">Area 1</span> - <input type = "text" id = "fzj.xg.helloWorld.area1" name = "fzj.xg.helloWorld.area1" class = "form-control" placeholder="Select a region" value = ""> - </div> - - <div class = "input-group"> - <span class = "input-group-addon">Area 2</span> - <input type = "text" id = "fzj.xg.helloWorld.area2" name = "fzj.xg.helloWorld.area2" class = "form-control" placeholder="Select a region" value = ""> - </div> - - <hr class = "col-md-10"> - - <div class = "col-md-12"> - Select genes of interest: - </div> - <div class = "input-group"> - <input type = "text" id = "fzj.xg.helloWorld.genes" name = "fzj.xg.helloWorld.genes" class = "form-control" placeholder = "Genes of interest ..."> - <span class = "input-group-btn"> - <button id = "fzj.xg.helloWorld.addgenes" name = "fzj.xg.helloWorld.addgenes" class = "btn btn-default" type = "button">Add</button> - </span> - </div> - - <hr class = "col-md-10"> - - <button id = "fzj.xg.helloWorld.submit" name = "fzj.xg.helloWorld.submit" type = "button" class = "btn btn-default btn-block">Submit</button> - - <hr class = "col-md-10"> - - <div class = "col-md-12" id = "fzj.xg.helloWorld.result"> - - </div> -</form> -``` - -*NB* -- *bootstrap 3.3.6* css is already included for templating. -- keep in mind of the widget width restriction (400px) when crafting the template -- whilst there are no vertical limits on the widget, contents can be rendered outside the viewport. Consider setting the *max-height* attribute. -- your template and script will interact with each other likely via *element id*. As a result, it is highly recommended that unique id's are used. Please adhere to the convention: **AFFILIATION.AUTHOR.PACKAGENAME.ELEMENTID** - ---- - -## Script JS - -The script will always be appended **after** the rendering of the template. - -```javascript -(()=>{ - /* your code here */ - - if(interactiveViewer.pluginControl['fzj.xg.helloWorld'].initState){ - /* init plugin with initState */ - } - - const submitButton = document.getElemenById('fzj.xg.helloWorld.submit') - submitButton.addEventListener('click',(ev)=>{ - console.log('submit button was clicked') - }) -})() -``` -*NB* -- JS is loaded and executed **before** the attachment of DOM (template). This is to allow webcomponents have a chance to be loaded. If your script needs the DOM to be attached, use a `setTimeout` callback to delay script execution. -- ensure the script is scoped locally, instead of poisoning the global scope -- for every observable subscription, call *unsubscribe()* in the *onShutdown* callback -- some frameworks such as *jquery2*, *jquery3*, *react/reactdom* and *webcomponents* can be loaded via *interactiveViewer.pluinControl.loadExternalLibraries([LIBRARY_NAME_1, LIBRARY_NAME_2])*. if the libraries are loaded, remember to hook *interactiveViewer.pluginControl.unloadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2])* in the *onShutdown* callback -- when/if using webcomponents, please be aware that the `connectedCallback()` and `disconnectedCallback()` will be called everytime user toggle between *floating* and *docked* modes. -- when user navigate to a new template all existing widgets will be destroyed, unless the `persistency` is set to `true` in `manifest.json`. -- for a list of APIs, see [plugin_api.md](plugin_api.md) diff --git a/src/plugin_examples/migrationGuide.md b/src/plugin_examples/migrationGuide.md deleted file mode 100644 index fcd5e040b9333b262dbbac60b6d3237859fe7f1c..0000000000000000000000000000000000000000 --- a/src/plugin_examples/migrationGuide.md +++ /dev/null @@ -1,51 +0,0 @@ -Plugin Migration Guide (v0.1.0 => v0.2.0) -====== -Plugin APIs have changed drastically from v0.1.0 to v0.2.0. Here is a list of plugin API from v0.1.0, and how it has changed moving to v0.2.0. - -**n.b.** `webcomponents-lite.js` is no longer included by default. You will need to request it explicitly with `window.interactiveViewer.pluginControl.loadExternalLibraries()` and unload it once you are done. - ---- - -- ~~*window.nehubaUI*~~ removed - - ~~*metadata*~~ => **window.interactiveViewer.metadata** - - ~~*selectedTemplate* : nullable Object~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead - - ~~*availableTemplates* : Array of TemplateDescriptors (empty array if no templates are available)~~ => **window.interactiveViewer.metadata.loadedTemplates** - - ~~*selectedParcellation* : nullable Object~~ removed. use **window.interactiveViewer.metadata.selectedParcellationBSubject** instead - - ~~*selectedRegions* : Array of Object (empty array if no regions are selected)~~ removed. use **window.interactiveViewer.metadata.selectedRegionsBSubject** instead - -- ~~window.pluginControl['YOURPLUGINNAME'] *nb: may be undefined if yourpluginname is incorrect*~~ => **window.interactiveViewer.pluginControl[YOURPLUGINNAME]** - - blink(sec?:number) : Function that causes the floating widget to blink, attempt to grab user attention - - ~~pushMessage(message:string) : Function that pushes a message that are displayed as a popover if the widget is minimised. No effect if the widget is not miniminised.~~ removed - - shutdown() : Function that causes the widget to shutdown dynamically. (triggers onShutdown callback) - - onShutdown(callback) : Attaches a callback function, which is called when the plugin is shutdown. - -- ~~*window.viewerHandle*~~ => **window.interactiveViewer.viewerHandle** - - ~~*loadTemplate(TemplateDescriptor)* : Function that loads a new template~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead - - ~~*onViewerInit(callback)* : Functional that allows a callback function to be called just before a nehuba viewer is initialised~~ removed - - ~~*afterViewerInit(callback)* : Function that allows a callback function to be called just after a nehuba viewer is initialised~~ removed - - ~~*onViewerDestroy(callback)* : Function that allows a callback function be called just before a nehuba viewer is destroyed~~ removed - - ~~*onParcellationLoading(callback)* : Function that allows a callback function to be called just before a parcellation is selected~~ removed - - ~~*afterParcellationLoading(callback)* : Function that allows a callback function to be called just after a parcellation is selected~~ removed - - *setNavigationLoc(loc,realSpace?)* : Function that teleports to loc : number[3]. Optional argument to determine if the loc is in realspace (default) or voxelspace. - - ~~*setNavigationOrientation(ori)* : Function that teleports to ori : number[4]. (Does not work currently)~~ => **setNavigationOri(ori)** (still non-functional) - - *moveToNavigationLoc(loc,realSpace?)* : same as *setNavigationLoc(loc,realSpace?)*, except moves to target location over 500ms. - - *showSegment(id)* : Function that selectes a segment in the viewer and UI. - - *hideSegment(id)* : Function that deselects a segment in the viewer and UI. - - *showAllSegments()* : Function that selects all segments. - - *hideAllSegments()* : Function that deselects all segments. - - *loadLayer(layerObject)* : Function that loads a custom neuroglancer compatible layer into the viewer (e.g. precomputed, NIFTI, etc). Does not influence UI. - - *mouseEvent* RxJs Observable. Read more at [rxjs doc](http://reactivex.io/rxjs/) - - *mouseEvent.filter(filterFn:({eventName : String, event: Event})=>boolean)* returns an Observable. Filters the event stream according to the filter function. - - *mouseEvent.map(mapFn:({eventName : String, event: Event})=>any)* returns an Observable. Map the event stream according to the map function. - - *mouseEvent.subscribe(callback:({eventName : String , event : Event})=>void)* returns an Subscriber instance. Call *Subscriber.unsubscribe()* when done to avoid memory leak. - - *mouseOverNehuba* RxJs Observable. Read more at [rxjs doc](http://reactivex.io/rxjs) - - *mouseOverNehuba.filter* && *mouseOvernehuba.map* see above - - *mouseOverNehuba.subscribe(callback:({nehubaOutput : any, foundRegion : any})=>void)* - -- ~~*window.uiHandle*~~ => **window.interactiveViewer.uiHandle** - - ~~*onTemplateSelection(callback)* : Function that allows a callback function to be called just after user clicks to navigate to a new template, before *selectedTemplate* is updated~~ removed. use **window.interactiveViewer.metadata.selectedTemplateBSubject** instead - - ~~*afterTemplateSelection(callback)* : Function that allows a callback function to be called after the template selection process is complete, and *selectedTemplate* is updated~~ removed - - ~~*onParcellationSelection(callback)* : Function that attach a callback function to user selecting a different parcellation~~ removed. use **window.interactiveViewer.metadata.selectedParcellationBSubject** instead. - - ~~*afterParcellationSelection(callback)* : Function that attach a callback function to be called after the parcellation selection process is complete and *selectedParcellation* is updated.~~ removed - - *modalControl* - - ~~*getModalHandler()* : Function returning a handler to change/show/hide/listen to a Modal.~~ removed \ No newline at end of file diff --git a/src/plugin_examples/plugin1/manifest.json b/src/plugin_examples/plugin1/manifest.json deleted file mode 100644 index 0813f787c22dc71f22c3d14bbd20de2716d27068..0000000000000000000000000000000000000000 --- a/src/plugin_examples/plugin1/manifest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name":"fzj.xg.exmaple.0_0_1", - "displayName": "Example Plugin (v0.0.1)", - "templateURL": "http://HOSTNAME/test.html", - "scriptURL": "http://HOSTNAME/script.js", - "initState": { - "key1": "val1", - "key2": { - "key21": "val21" - } - }, - "initStateUrl": "http://HOSTNAME/state?id=007", - "persistency": false, - "description": "description of example plugin", - "desc": "desc of example plugin", - "homepage": "http://HOSTNAME/home.html", - "authors": "Xiaoyun Gui <x.gui@fz-juelich.de>" -} \ No newline at end of file diff --git a/src/plugin_examples/plugin_api.md b/src/plugin_examples/plugin_api.md deleted file mode 100644 index 6954f15b52d7900e14a493376423c61697dacb82..0000000000000000000000000000000000000000 --- a/src/plugin_examples/plugin_api.md +++ /dev/null @@ -1,416 +0,0 @@ -# Plugin APIs - -## window.interactiveViewer - -### metadata - -#### selectedTemplateBSubject - -BehaviourSubject that emits a TemplateDescriptor object whenever a template is selected. Emits null onInit. - -#### selectedParcellationBSubject - -BehaviourSubject that emits a ParcellationDescriptor object whenever a parcellation is selected. n.b. selecting a new template automatically select the first available parcellation. Emits null onInit. - -#### selectedRegionsBSubject - -BehaviourSubject that emits an Array of RegionDescriptor objects whenever the list of selected regions changes. Emits empty array onInit. - -#### loadedTemplates - -Array of TemplateDescriptor objects. Loaded asynchronously onInit. - -#### layersRegionLabelIndexMap - -Map of layer name to Map of labelIndex (used by neuroglancer and nehuba) to the corresponding RegionDescriptor object. - -### viewerHandle - -> **nb** `viewerHandle` may be undefined at any time (user be yet to select an atlas, user could have unloaded an atlas. ...etc) - -#### setNavigationLoc(coordinates, realspace?:boolean) - -Function that teleports the navigation state to coordinates : [x:number,y:number,z:number]. Optional arg determine if the set of coordinates is in realspace (default) or voxelspace. - -#### moveToNavigationLoc(coordinates,realspace?:boolean) - -same as *setNavigationLoc(coordinates,realspace?)*, except the action is carried out over 500ms. - -#### setNavigationOri(ori) - -(NYI) Function that sets the orientation state of the viewer. - -#### moveToNavigationOri(ori) - -(NYI) same as *setNavigationOri*, except the action is carried out over 500ms. - -#### showSegment(labelIndex) - -Function that shows a specific segment. Will trigger *selectedRegionsBSubject*. - -#### hideSegment(labelIndex) - -Function that hides a specific segment. Will trigger *selectRegionsBSubject* - -#### showAllSegments() - -Function that shows all segments. Will trigger *selectRegionsBSubject* - -#### hideAllSegments() -Function that hides all segments. Will trigger *selectRegionBSubject* - -#### getLayersSegmentColourMap() - -Call to get Map of layer name to Map of label index to colour map - -#### applyLayersColourMap - -Function that applies a custom colour map. - -#### loadLayer(layerObject) - -Function that loads *ManagedLayersWithSpecification* directly to neuroglancer. Returns the values of the object successfully added. **n.b.** advanced feature, will likely break other functionalities. **n.b.** if the layer name is already taken, the layer will not be added. - -```javascript -const obj = { - 'advanced layer' : { - type : 'image', - source : 'nifti://http://example.com/data/nifti.nii', - }, - 'advanced layer 2' : { - type : 'mesh', - source : 'vtk://http://example.com/data/vtk.vtk' - } -} -const returnValue = window.interactiveViewer.viewerHandle.loadLayer(obj) -/* loads two layers, an image nifti layer and a mesh vtk layer */ - -console.log(returnValue) -/* prints - -[{ - type : 'image', - source : 'nifti...' -}, -{ - type : 'mesh', - source : 'vtk...' -}] -*/ -``` - -#### removeLayer(layerObject) - -Function that removes *ManagedLayersWithSpecification*, returns an array of the names of the layers removed. - -**n.b.** advanced feature. may break other functionalities. - -```js -const obj = { - 'name' : /^PMap/ -} -const returnValue = window.interactiveViewer.viewerHandle.removeLayer(obj) - -console.log(returnValue) -/* prints -['PMap 001','PMap 002'] -*/ -``` - -#### add3DLandmarks(landmarks) - -adds landmarks to both the perspective view and slice view. - -_input_ - -| input | type | desc | -| --- | --- | --- | -| landmarks | array | an array of `landmarks` to be rendered by the viewer | - -A landmark object consist of the following keys: - -| key | type | desc | required | -| --- | --- | --- | --- | -| id | string | id of the landmark | yes | -| name | string | name of the landmark | | -| position | [number, number, number] | position (in mm) | yes | -| color | [number, number, number] | rgb of the landmark | | - - -```js -const landmarks = [{ - id : `fzj-xg-jugex-1`, - position : [0,0,0] -},{ - id : `fzj-xg-jugex-2`, - position : [22,27,-1], - color: [255, 0, 255] -}] -window.interactiveViewer.viewerHandle.add3DLandmarks(landmarks) - -/* adds landmarks in perspective view and slice view */ -``` - -#### remove3DLandmarks(IDs) - -removes the landmarks by their IDs - -```js -window.interactiveViewer.viewerHandle - .remove3DLandmarks(['fzj-xg-jugex-1', 'fzj-xg-jugex-2']) -/* removes the landmarks added above */ -``` - -#### setLayerVisibility(layerObject, visible) - -Function that sets the visibility of a layer. Returns the names of all the layers that are affected as an Array of string. - -```js -const obj = { - 'type' : 'segmentation' -} - -window.interactiveViewer.viewerHandle.setLayerVisibility(obj,false) - -/* turns off all the segmentation layers */ -``` - -#### mouseEvent - -Subject that emits an object shaped `{ eventName : string, event: event }` when a user triggers a mouse event on the viewer. - - -#### mouseOverNehuba - -**deprecating** use mouseOverNehubaUI instead - -BehaviourSubject that emits an object shaped `{ nehubaOutput : number | null, foundRegion : RegionDescriptor | null }` - -#### mouseOverNehubaUI - -`Observable<{ landmark, segments, customLandmark }>`. - -**nb** it is a known issue that if customLandmarks are destroyed/created while user mouse over the custom landmark this observable will emit `{ customLandmark: null }` - -### uiHandle - -#### getModalHandler() - -returns a modalHandler object, which has the following methods/properties: - -##### hide() - -Dynamically hides the modal - -##### show() - -Shows the modal - -##### title - -title of the modal (String) - -##### body - -body of the modal shown (String) - -##### footer - -footer of the modal (String) - -##### dismissable - -whether the modal is dismissable on click backdrop/esc key (Boolean) - -*n.b. if true, users will not be able to interact with the viewer unless you specifically call `handler.hide()`* - -#### launchNewWidget(manifest) - -returns a Promise. expects a JSON object, with the same key value as a plugin manifest. the *name* key must be unique, or the promise will be rejected. - -#### getUserInput(config) - -returns a Promise, resolves when user confirms, rejects when user cancels. expects config object object with the following structure: - -```javascript -const config = { - "title": "Title of the modal", // default: "Message" - "message":"Message to be seen by the user.", // default: "" - "placeholder": "Start typing here", // default: "Type your response here" - "defaultValue": "42" // default: "" - "iconClass":"fas fa-save" // default fas fa-save, set to falsy value to disable -} -``` - -#### getUserConfirmation(config) - -returns a Promise, resolves when user confirms, rejects when user cancels. expects config object object with the following structure: - -```javascript -const config = { - "title": "Title of the modal", // default: "Message" - "message":"Message to be seen by the user." // default: "" -} -``` - -#### getUserToSelectARegion(message) - -**To be deprecated** - -_input_ - -| input | type | desc | -| --- | --- | --- | -| message | `string` | human readable message displayed to the user | -| spec.type | `'POINT'` `'PARCELLATION_REGION'` **default** | type of region to be returned. | - -_returns_ - -`Promise`, resolves to return array of region clicked, rejects with error object `{ userInitiated: boolean }` - -Requests user to select a region of interest. Resolving to the region selected by the user. Rejects if either user cancels by pressing `Esc` or `Cancel`, or by developer calling `cancelPromise` - -#### getUserToSelectRoi(message, spec) - -_input_ - -| input | type | desc | -| --- | --- | --- | -| message | `string` | human readable message displayed to the user | -| spec.type | `POINT` `PARCELLATION_REGION` | type of ROI to be returned. | - -_returns_ - -`Promise` - -**resolves**: return `{ type, payload }`. `type` is the same as `spec.type`, and `payload` differs depend on the type requested: - -| type | payload | example | -| --- | --- | --- | -| `POINT` | array of number in mm | `[12.2, 10.1, -0.3]` | -| `PARCELLATION_REGOIN` | non empty array of region selected | `[{ "layer": { "name" : " viewer specific layer name " }, "segment": {} }]` | - -**rejects**: with error object `{ userInitiated: boolean }` - -Requests user to select a region of interest. If the `spec.type` input is missing, it is assumed to be `'PARCELLATION_REGION'`. Resolving to the region selected by the user. Rejects if either user cancels by pressing `Esc` or `Cancel`, or by developer calling `cancelPromise` - -#### cancelPromise(promise) - -returns `void` - -_input_ - -| input | type | desc | -| --- | --- | --- | -| promise | `Promise` | Reference to the __exact__ promise returned by `uiHnandle` methods | - -Cancel the request to select a parcellation region. - -_usage example_ - -```javascript - -(() => { - const pr = interactive.uiHandle.getUserToSelectARegion(`webJuGEx would like you to select a region`) - - pr.then(region => { }) - .catch(console.warn) - - /* - * do NOT do - * - * const pr = interactive.uiHandle.getUserToSelectARegion(`webJuGEx would like you to select a region`) - * .then(region => { }) - * -catch(console.warn) - * - * the promise passed to `cancelPromise` must be the exact promise returned. - * by chaining then/catch, a new reference is returned - */ - - setTimeout(() => { - try { - interactive.uiHandle.cancelPromise(pr) - } catch (e) { - // if the promise has been fulfilled (by resolving or user cancel), cancelPromise will throw - } - }, 5000) -})() -``` - -### pluginControl - -#### loadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2]) - -Function that loads external libraries. Pass the name of the libraries as an Array of string, and returns a Promise. When promise resolves, the libraries are loaded. - -**n.b.** while unlikely, there is a possibility that multiple requests to load external libraries in quick succession can cause the promise to resolve before the library is actually loaded. - -```js -const currentlySupportedLibraries = ['jquery@2','jquery@3','webcomponentsLite@1.1.0','react@16','reactdom@16','vue@2.5.16'] - -window.interactivewViewer.loadExternalLibraries(currentlySupportedLibraries) - .then(() => { - /* loaded */ - }) - .catch(e=>console.warn(e)) - -``` - -#### unloadExternalLibraries([LIBRARY_NAME_1,LIBRARY_NAME_2]) - -unloading the libraries (should be called on shutdown). - -#### *[PLUGINNAME]* - -returns a plugin handler. This would be how to interface with the plugins. - -##### blink() - -Function that causes the floating widget to blink, attempt to grab user attention (silently fails if called on startup). - -##### setProgressIndicator(val:number|null) - -Set the progress of the plugin. Useful for giving user feedbacks on the progress of a long running process. Call the function with null to unset the progress. - -##### shutdown() - -Function that causes the widget to shutdown dynamically. (triggers onShutdown callback, silently fails if called on startup) - -##### onShutdown(callback) - -Attaches a callback function, which is called when the plugin is shutdown. - -##### initState - -passed from `manifest.json`. Useful for setting initial state of the plugin. Can be any JSON valid value (array, object, string). - -##### initStateUrl - -passed from `manifest.json`. Useful for setting initial state of the plugin. Can be any JSON valid value (array, object, string). - -##### setInitManifestUrl(url|null) - -set/unset the url for a manifest json that will be fetched on atlas viewer startup. the argument should be a valid URL, has necessary CORS header, and returns a valid manifest json file. null will unset the search param. Useful for passing/preserving state. If called multiple times, the last one will take effect. - -```js -const pluginHandler = window.interactiveViewer.pluginControl[PLUGINNAME] - -const subscription = window.interactiveViewer.metadata.selectedTemplateBSubject.subscribe(template=>console.log(template)) - -fetch(`http://YOUR_BACKEND.com/API_ENDPOINT`) - .then(data=>pluginHandler.blink(20)) - -pluginHandler.onShutdown(()=>{ - subscription.unsubscribe() -}) -``` - ------- - -## window.nehubaViewer - -nehuba object, exposed if developer would like to use it - -## window.viewer - -neuroglancer object, exposed if developer would like to use it \ No newline at end of file diff --git a/src/routerModule/cipher.ts b/src/routerModule/cipher.ts index 9ad21bc8c036336c1bc71f4a2dc0122603a3a681..e98d9b3f39fb8f2193c64da6094fbe0db0fae868 100644 --- a/src/routerModule/cipher.ts +++ b/src/routerModule/cipher.ts @@ -10,84 +10,22 @@ * So performance is not really that important (Also, need to learn bitwise operation) */ -const cipher = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-' -export const separator = "." -const negString = '~' -const encodeInt = (number: number) => { - if (number % 1 !== 0) { throw new Error('cannot encodeInt on a float. Ensure float flag is set') } - if (isNaN(Number(number)) || number === null || number === Number.POSITIVE_INFINITY) { throw new Error('The input is not valid') } - - let residual: number - let result = '' - - if (number < 0) { - result += negString - residual = Math.floor(number * -1) - } else { - residual = Math.floor(number) - } - - /* eslint-disable-next-line no-constant-condition */ - while (true) { - result = cipher.charAt(residual % 64) + result - residual = Math.floor(residual / 64) - - if (residual === 0) { - break - } - } - return result -} - -interface IB64EncodingOption { - float: boolean +import { sxplrNumB64Enc } from "common/util" +const { + separator, + cipher, + encodeNumber, + decodeToNumber, +} = sxplrNumB64Enc + +export { + separator, + cipher, + encodeNumber, + decodeToNumber, } -const defaultB64EncodingOption = { - float: false, -} - -export const encodeNumber: - (number: number, option?: IB64EncodingOption) => string = - (number: number, { float = false }: IB64EncodingOption = defaultB64EncodingOption) => { - if (!float) { return encodeInt(number) } else { - const floatArray = new Float32Array(1) - floatArray[0] = number - const intArray = new Uint32Array(floatArray.buffer) - const castedInt = intArray[0] - return encodeInt(castedInt) - } - } - -const decodetoInt = (encodedString: string) => { - let _encodedString - let negFlag = false - if (encodedString.slice(-1) === negString) { - negFlag = true - _encodedString = encodedString.slice(0, -1) - } else { - _encodedString = encodedString - } - return (negFlag ? -1 : 1) * [..._encodedString].reduce((acc, curr) => { - const index = cipher.indexOf(curr) - if (index < 0) { throw new Error(`Poisoned b64 encoding ${encodedString}`) } - return acc * 64 + index - }, 0) -} - -export const decodeToNumber: - (encodedString: string, option?: IB64EncodingOption) => number = - (encodedString: string, {float = false} = defaultB64EncodingOption) => { - if (!float) { return decodetoInt(encodedString) } else { - const _int = decodetoInt(encodedString) - const intArray = new Uint32Array(1) - intArray[0] = _int - const castedFloat = new Float32Array(intArray.buffer) - return castedFloat[0] - } - } - /** * see https://stackoverflow.com/questions/53051415/can-you-disable-auxiliary-secondary-routes-in-angular * need to encode brackets diff --git a/src/routerModule/routeStateTransform.service.ts b/src/routerModule/routeStateTransform.service.ts index 10f003638a6f8bed8c281e40777134572fe42c5c..dc7da45d8a9eed4d3b8c4edbe31c409864d09741 100644 --- a/src/routerModule/routeStateTransform.service.ts +++ b/src/routerModule/routeStateTransform.service.ts @@ -146,8 +146,11 @@ export class RouteStateTransformSvc { const pluginStates = fullPath.queryParams['pl'] if (pluginStates) { try { - const arrPluginStates = JSON.parse(pluginStates) - returnState["[state.plugins]"].initManifests = arrPluginStates.map(url => [plugins.INIT_MANIFEST_SRC, url] as [string, string]) + const arrPluginStates: string[] = JSON.parse(pluginStates) + if (arrPluginStates.length > 1) throw new Error(`can only initialise one plugin at a time`) + returnState["[state.plugins]"].initManifests = { + [plugins.INIT_MANIFEST_SRC]: arrPluginStates + } } catch (e) { /** * parsing plugin error diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts index 3500cc3dcba1b6d4aa412d38026fd28cdf31ca3a..3359055f7b70b395772c5d27842dffee46b1b374 100644 --- a/src/routerModule/router.service.ts +++ b/src/routerModule/router.service.ts @@ -10,9 +10,8 @@ import { scan } from 'rxjs/operators' import { RouteStateTransformSvc } from "./routeStateTransform.service"; import { SAPI } from "src/atlasComponents/sapi"; import { generalActions } from "src/state"; -/** - * http://localhost:8080/#/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2/p:minds:core:parcellationatlas:v1.0.0:94c1125b-b87e-45e4-901c-00daee7f2579-290/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIy..0.14gY0~.14gY0..1LSm - */ + + @Injectable({ providedIn: 'root' }) diff --git a/src/share/saneUrl/saneUrl.component.spec.ts b/src/share/saneUrl/saneUrl.component.spec.ts index 5ab173950c92148144b7b3025f63cddf95205a30..982ecb76fb47ca954a5da6c9f5c792c69f3118ba 100644 --- a/src/share/saneUrl/saneUrl.component.spec.ts +++ b/src/share/saneUrl/saneUrl.component.spec.ts @@ -1,14 +1,13 @@ -import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing' -import { ShareModule } from '../share.module' +import { TestBed, fakeAsync, tick, flush, ComponentFixture } from '@angular/core/testing' import { SaneUrl } from './saneUrl.component' -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing' import { By } from '@angular/platform-browser' import { BACKENDURL } from 'src/util/constants' import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { SaneUrlSvc } from './saneUrl.service' import { AngularMaterialModule } from 'src/sharedModules' import { CUSTOM_ELEMENTS_SCHEMA, Directive } from '@angular/core' -import { of } from 'rxjs' +import { of, throwError } from 'rxjs' +import { NotFoundError } from '../type' const inputCss = `input[aria-label="Custom link"]` const submitCss = `button[aria-label="Create custom link"]` @@ -25,15 +24,22 @@ class AuthStateDummy { describe('> saneUrl.component.ts', () => { describe('> SaneUrl', () => { + const mockSaneUrlSvc = { + saneUrlroot: 'saneUrlroot', + getKeyVal: jasmine.createSpy('getKeyVal'), + setKeyVal: jasmine.createSpy('setKeyVal'), + } beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ - HttpClientTestingModule, NoopAnimationsModule, AngularMaterialModule, ], providers: [ - SaneUrlSvc, + { + provide: SaneUrlSvc, + useValue: mockSaneUrlSvc + } ], declarations: [ SaneUrl, @@ -43,11 +49,18 @@ describe('> saneUrl.component.ts', () => { CUSTOM_ELEMENTS_SCHEMA ] }).compileComponents() + + mockSaneUrlSvc.getKeyVal.and.returnValue( + of('foo-bar') + ) + mockSaneUrlSvc.setKeyVal.and.returnValue( + of('OK') + ) }) afterEach(() => { - const ctrl = TestBed.inject(HttpTestingController) - ctrl.verify() + mockSaneUrlSvc.getKeyVal.calls.reset() + mockSaneUrlSvc.setKeyVal.calls.reset() }) it('> can be created', () => { @@ -112,215 +125,188 @@ describe('> saneUrl.component.ts', () => { const input = fixture.debugElement.query( By.css( inputCss ) ) }) - it('> on entering string in input, makes debounced GET request', fakeAsync(() => { - - const value = 'test_1' - - const httpTestingController = TestBed.inject(HttpTestingController) - - // Necessary to detectChanges, or formControl will not initialise properly - // See https://stackoverflow.com/a/56600762/6059235 - const fixture = TestBed.createComponent(SaneUrl) - fixture.detectChanges() - - // Set value - fixture.componentInstance.customUrl.setValue(value) - - tick(500) - - const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`) - req.flush(200) - })) - - it('> on 200 response, show error', fakeAsync(() => { - - const value = 'test_1' - - const httpTestingController = TestBed.inject(HttpTestingController) - - // Necessary to detectChanges, or formControl will not initialise properly - // See https://stackoverflow.com/a/56600762/6059235 - const fixture = TestBed.createComponent(SaneUrl) - fixture.detectChanges() - - // Set value - fixture.componentInstance.customUrl.setValue(value) - - tick(500) - - const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`) - req.flush('OK') - - // Expect validator to fail catch it - expect(fixture.componentInstance.customUrl.invalid).toEqual(true) - - // on change detection, UI should catch it - fixture.detectChanges() - - const input = fixture.debugElement.query( By.css( inputCss ) ) - - const submit = fixture.debugElement.query( By.css( submitCss ) ) - const disabled = !!submit.attributes['disabled'] - expect(disabled.toString()).toEqual('true') - })) - - it('> on 404 response, show available', fakeAsync(() => { - - const value = 'test_1' - - const httpTestingController = TestBed.inject(HttpTestingController) - - // Necessary to detectChanges, or formControl will not initialise properly - // See https://stackoverflow.com/a/56600762/6059235 - const fixture = TestBed.createComponent(SaneUrl) - fixture.detectChanges() - - // Set value - fixture.componentInstance.customUrl.setValue(value) - - tick(500) - - const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`) - req.flush('some reason', { status: 404, statusText: 'Not Found.' }) - - // Expect validator to fail catch it - expect(fixture.componentInstance.customUrl.invalid).toEqual(false) - - // on change detection, UI should catch it - fixture.detectChanges() - - const input = fixture.debugElement.query( By.css( inputCss ) ) - - const submit = fixture.debugElement.query( By.css( submitCss ) ) - const disabled = !!submit.attributes['disabled'] - expect(disabled.toString()).toEqual('false') - })) - - it('> on other error codes, show invalid', fakeAsync(() => { - - const value = 'test_1' - - const httpTestingController = TestBed.inject(HttpTestingController) - - // Necessary to detectChanges, or formControl will not initialise properly - // See https://stackoverflow.com/a/56600762/6059235 - const fixture = TestBed.createComponent(SaneUrl) - fixture.detectChanges() - - // Set value - fixture.componentInstance.customUrl.setValue(value) - - tick(500) - - const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`) - req.flush('some reason', { status: 401, statusText: 'Unauthorised.' }) - - // Expect validator to fail catch it - expect(fixture.componentInstance.customUrl.invalid).toEqual(true) - - // on change detection, UI should catch it - fixture.detectChanges() - - const input = fixture.debugElement.query( By.css( inputCss ) ) - - const submit = fixture.debugElement.query( By.css( submitCss ) ) - const disabled = !!submit.attributes['disabled'] - expect(disabled.toString()).toEqual('true') - })) - - it('> on click create link btn calls correct API', fakeAsync(() => { - - const value = 'test_1' - - const httpTestingController = TestBed.inject(HttpTestingController) - - // Necessary to detectChanges, or formControl will not initialise properly - // See https://stackoverflow.com/a/56600762/6059235 - const fixture = TestBed.createComponent(SaneUrl) - fixture.detectChanges() - - // Set value - fixture.componentInstance.customUrl.setValue(value) - - tick(500) - - const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`) - req.flush('some reason', { status: 404, statusText: 'Not Found.' }) - - fixture.detectChanges() - flush() - - const submit = fixture.debugElement.query( By.css( submitCss ) ) - const disabled = !!submit.attributes['disabled'] - expect(disabled.toString()).toEqual('false') - - submit.triggerEventHandler('click', {}) - - fixture.detectChanges() - - const disabledInProgress = !!submit.attributes['disabled'] - expect(disabledInProgress.toString()).toEqual('true') - - const req2 = httpTestingController.expectOne({ - method: 'POST', - url: `${BACKENDURL}saneUrl/${value}` + describe("> on valid input", () => { + let saneUrlCmp: SaneUrl + let fixture: ComponentFixture<SaneUrl> + const stateTobeSaved = 'foo-bar' + beforeEach(() => { + // Necessary to detectChanges, or formControl will not initialise properly + // See https://stackoverflow.com/a/56600762/6059235 + fixture = TestBed.createComponent(SaneUrl) + saneUrlCmp = fixture.componentInstance + saneUrlCmp.stateTobeSaved = stateTobeSaved + fixture.detectChanges() }) - - req2.flush({}) - - fixture.detectChanges() - - const disabledAfterComplete = !!submit.attributes['disabled'] - expect(disabledAfterComplete.toString()).toEqual('true') - - const cpyBtn = fixture.debugElement.query( By.css( copyBtnCss ) ) - expect(cpyBtn).toBeTruthy() - })) - - it('> on click create link btn fails show result', fakeAsync(() => { - - const value = 'test_1' - - const httpTestingController = TestBed.inject(HttpTestingController) - - // Necessary to detectChanges, or formControl will not initialise properly - // See https://stackoverflow.com/a/56600762/6059235 - const fixture = TestBed.createComponent(SaneUrl) - fixture.detectChanges() - - // Set value - fixture.componentInstance.customUrl.setValue(value) - - tick(500) - - const req = httpTestingController.expectOne(`${BACKENDURL}saneUrl/${value}`) - req.flush('some reason', { status: 404, statusText: 'Not Found.' }) - - fixture.detectChanges() - flush() - - const submit = fixture.debugElement.query( By.css( submitCss ) ) - const disabled = !!submit.attributes['disabled'] - expect(disabled.toString()).toEqual('false') - - submit.triggerEventHandler('click', {}) - - fixture.detectChanges() - - const disabledInProgress = !!submit.attributes['disabled'] - expect(disabledInProgress.toString()).toEqual('true') - - const req2 = httpTestingController.expectOne({ - method: 'POST', - url: `${BACKENDURL}saneUrl/${value}` + it('> on entering string in input, makes debounced GET request', fakeAsync(() => { + + const value = 'test_1' + + // Set value + fixture.componentInstance.customUrl.setValue(value) + + tick(500) + + expect(mockSaneUrlSvc.getKeyVal).toHaveBeenCalledOnceWith(value) + })) + + describe("> on 200", () => { + it("> show error", fakeAsync(() => { + + const value = 'test_1' + + // Set value + fixture.componentInstance.customUrl.setValue(value) + + tick(500) + + // Expect validator to fail catch it + expect(fixture.componentInstance.customUrl.invalid).toEqual(true) + + // on change detection, UI should catch it + fixture.detectChanges() + + const input = fixture.debugElement.query( By.css( inputCss ) ) + + const submit = fixture.debugElement.query( By.css( submitCss ) ) + const disabled = !!submit.attributes['disabled'] + expect(disabled.toString()).toEqual('true') + })) + }) + + describe('> on 404', () => { + beforeEach(() => { + mockSaneUrlSvc.getKeyVal.and.returnValue( + throwError(new NotFoundError('not found')) + ) + }) + it("> should available", fakeAsync(() => { + + const value = 'test_1' + + // Set value + fixture.componentInstance.customUrl.setValue(value) + + tick(500) + + // Expect validator to fail catch it + expect(fixture.componentInstance.customUrl.invalid).toEqual(false) + + // on change detection, UI should catch it + fixture.detectChanges() + + const input = fixture.debugElement.query( By.css( inputCss ) ) + + const submit = fixture.debugElement.query( By.css( submitCss ) ) + const disabled = !!submit.attributes['disabled'] + expect(disabled.toString()).toEqual('false') + })) }) + + describe("> on other error", () => { + beforeEach(() => { + + mockSaneUrlSvc.getKeyVal.and.returnValue( + throwError(new Error('other errors')) + ) + }) + it("> show invalid", fakeAsync(() => { + const value = 'test_1' + + // Set value + fixture.componentInstance.customUrl.setValue(value) + + tick(500) + + // Expect validator to fail catch it + expect(fixture.componentInstance.customUrl.invalid).toEqual(true) + + // on change detection, UI should catch it + fixture.detectChanges() + + const input = fixture.debugElement.query( By.css( inputCss ) ) + + const submit = fixture.debugElement.query( By.css( submitCss ) ) + const disabled = !!submit.attributes['disabled'] + expect(disabled.toString()).toEqual('true') + })) + }) + + describe("> on click create link", () => { + beforeEach(() => { + mockSaneUrlSvc.getKeyVal.and.returnValue( + throwError(new NotFoundError('not found')) + ) + }) + it("> calls correct service function", fakeAsync(() => { + + const value = 'test_1' + + // Set value + fixture.componentInstance.customUrl.setValue(value) + + tick(500) + + fixture.detectChanges() + flush() + + const submit = fixture.debugElement.query( By.css( submitCss ) ) + const disabled = !!submit.attributes['disabled'] + expect(disabled.toString()).toEqual('false') + + submit.triggerEventHandler('click', {}) + + fixture.detectChanges() + + const disabledInProgress = !!submit.attributes['disabled'] + expect(disabledInProgress.toString()).toEqual('true') + + fixture.detectChanges() + + const disabledAfterComplete = !!submit.attributes['disabled'] + expect(disabledAfterComplete.toString()).toEqual('true') + + const cpyBtn = fixture.debugElement.query( By.css( copyBtnCss ) ) + expect(cpyBtn).toBeTruthy() + })) + + describe("> on fail", () => { + beforeEach(() => { + mockSaneUrlSvc.setKeyVal.and.returnValue( + throwError(new Error(`some error`)) + ) + }) + it("> show result", fakeAsync(() => { + + const value = 'test_1' - req2.flush('Something went wrong', { statusText: 'Wrong status text', status: 500 }) - - fixture.detectChanges() - - const input = fixture.debugElement.query( By.css( inputCss ) ) - - })) + // Set value + fixture.componentInstance.customUrl.setValue(value) + + tick(500) + + fixture.detectChanges() + + const submit = fixture.debugElement.query( By.css( submitCss ) ) + const disabled = !!submit.attributes['disabled'] + expect(disabled.toString()).toEqual('false') + + submit.triggerEventHandler('click', {}) + + fixture.detectChanges() + + const disabledInProgress = !!submit.attributes['disabled'] + expect(disabledInProgress.toString()).toEqual('true') + + expect(mockSaneUrlSvc.setKeyVal).toHaveBeenCalledOnceWith(value, stateTobeSaved) + + fixture.detectChanges() + + const input = fixture.debugElement.query( By.css( inputCss ) ) + + })) + }) + }) + + }) }) }) diff --git a/src/share/saneUrl/saneUrl.service.ts b/src/share/saneUrl/saneUrl.service.ts index 8f3af1f3d55fcb9baec57d44ddbbda76ac24c2df..c5d9849f760ed90b7197ef8206b53db55ab1025d 100644 --- a/src/share/saneUrl/saneUrl.service.ts +++ b/src/share/saneUrl/saneUrl.service.ts @@ -4,23 +4,27 @@ import { throwError } from "rxjs"; import { catchError, mapTo } from "rxjs/operators"; import { BACKENDURL } from 'src/util/constants' import { IKeyValStore, NotFoundError } from '../type' +import { DISABLE_PRIORITY_HEADER } from "src/util/priority" @Injectable({ providedIn: 'root' }) export class SaneUrlSvc implements IKeyValStore{ - public saneUrlRoot = `${BACKENDURL}saneUrl/` + public saneUrlRoot = `${BACKENDURL}go/` constructor( private http: HttpClient ){ - + if (!BACKENDURL) { + const loc = window.location + this.saneUrlRoot = `${loc.protocol}//${loc.hostname}${!!loc.port ? (':' + loc.port) : ''}${loc.pathname}go/` + } } getKeyVal(key: string) { return this.http.get<Record<string, any>>( `${this.saneUrlRoot}${key}`, - { responseType: 'json' } + { responseType: 'json', headers: { [DISABLE_PRIORITY_HEADER]: '1' } } ).pipe( catchError((err, obs) => { const { status } = err @@ -35,7 +39,8 @@ export class SaneUrlSvc implements IKeyValStore{ setKeyVal(key: string, value: any) { return this.http.post( `${this.saneUrlRoot}${key}`, - value + value, + { headers: { [DISABLE_PRIORITY_HEADER]: '1' } } ).pipe( mapTo(`${this.saneUrlRoot}${key}`) ) diff --git a/src/share/share.module.ts b/src/share/share.module.ts index f3137eaadc83a3aaead101efe1b76cc600301409..ff7011249173857ff344bd58730f7e7881f9e675 100644 --- a/src/share/share.module.ts +++ b/src/share/share.module.ts @@ -8,6 +8,7 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { AuthModule } from "src/auth"; import { ShareSheetComponent } from "./shareSheet/shareSheet.component"; import { ShareDirective } from "./share.directive"; +import { StateModule } from "src/state"; @NgModule({ imports: [ @@ -17,6 +18,7 @@ import { ShareDirective } from "./share.directive"; FormsModule, ReactiveFormsModule, AuthModule, + StateModule, ], declarations: [ ClipboardCopy, diff --git a/src/share/shareSheet/shareSheet.component.ts b/src/share/shareSheet/shareSheet.component.ts index 8d4b9f963ebafc0b83850010bf8b18e70f7d6a15..664ffd89c8fe93bf1a7886d80bc83145e7c8b03a 100644 --- a/src/share/shareSheet/shareSheet.component.ts +++ b/src/share/shareSheet/shareSheet.component.ts @@ -1,7 +1,6 @@ -import { Component } from "@angular/core"; +import { Component, TemplateRef } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; import { ARIA_LABELS } from 'common/constants' -import { SaneUrl } from "../saneUrl/saneUrl.component" @Component({ selector: 'sxplr-share-sheet', @@ -18,7 +17,7 @@ export class ShareSheetComponent{ } - openShareSaneUrl(){ - this.dialog.open(SaneUrl, { ariaLabel: ARIA_LABELS.SHARE_CUSTOM_URL }) + openDialog(templateRef: TemplateRef<unknown>){ + this.dialog.open(templateRef, { ariaLabel: ARIA_LABELS.SHARE_CUSTOM_URL }) } } diff --git a/src/share/shareSheet/shareSheet.template.html b/src/share/shareSheet/shareSheet.template.html index cea5242302cf57ff1cc718ac1b2d98b7a9d79f0c..bc953043ba622d6af44df97a27215346bd03521b 100644 --- a/src/share/shareSheet/shareSheet.template.html +++ b/src/share/shareSheet/shareSheet.template.html @@ -17,8 +17,10 @@ <mat-list-item [attr.aria-label]="ARIA_LABELS.SHARE_CUSTOM_URL" [attr.tab-index]="10" - (click)="openShareSaneUrl()" + (click)="openDialog(saneUrlTmpl)" [matTooltip]="ARIA_LABELS.SHARE_CUSTOM_URL" + iav-state-aggregator + #stateAggregator="iavStateAggregator" > <mat-icon class="mr-4" @@ -32,3 +34,9 @@ </mat-list-item> </mat-nav-list> + + +<ng-template #saneUrlTmpl> + <iav-sane-url [stateTobeSaved]="stateAggregator.jsonifiedState$ | async"> + </iav-sane-url> +</ng-template> \ No newline at end of file diff --git a/src/state/atlasSelection/effects.spec.ts b/src/state/atlasSelection/effects.spec.ts index 61110e26b2d8b746e3c65c04dc89b07ddabd5b73..556e002c56f518ba5957629a178c5249ad37d234 100644 --- a/src/state/atlasSelection/effects.spec.ts +++ b/src/state/atlasSelection/effects.spec.ts @@ -1,16 +1,15 @@ -import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" import { TestBed } from "@angular/core/testing" import { provideMockActions } from "@ngrx/effects/testing" import { Action } from "@ngrx/store" import { MockStore, provideMockStore } from "@ngrx/store/testing" import { hot } from "jasmine-marbles" import { Observable, of, throwError } from "rxjs" -import { SAPI, SAPIModule, SapiRegionModel, SAPIParcellation, SapiAtlasModel, SapiSpaceModel, SapiParcellationModel } from "src/atlasComponents/sapi" +import { SAPI, SAPIModule, SapiRegionModel, SapiAtlasModel, SapiSpaceModel, SapiParcellationModel } from "src/atlasComponents/sapi" import { IDS } from "src/atlasComponents/sapi/constants" import { actions, selectors } from "." import { Effect } from "./effects" import * as mainActions from "../actions" -import { take } from "rxjs/operators" +import { atlasSelection } from ".." describe("> effects.ts", () => { describe("> Effect", () => { @@ -40,7 +39,6 @@ describe("> effects.ts", () => { const sapisvc = TestBed.inject(SAPI) const regions = await sapisvc.getParcRegions(IDS.ATLAES.HUMAN, IDS.PARCELLATION.JBA29, IDS.TEMPLATES.MNI152).toPromise() - hoc1left = regions.find(r => /hoc1/i.test(r.name) && /left/i.test(r.name)) if (!hoc1left) throw new Error(`cannot find hoc1 left`) hoc1leftCentroid = JSON.parse(JSON.stringify(hoc1left)) @@ -94,6 +92,98 @@ describe("> effects.ts", () => { }) }) + + describe("> onTemplateParcSelectionPostHook", () => { + describe("> 0", () => { + }) + describe("> 1", () => { + const currNavigation = { + orientation: [0, 0, 0, 1], + perspectiveOrientation: [0, 0, 0, 1], + perspectiveZoom: 1, + position: [1, 2, 3], + zoom: 1 + } + beforeEach(() => { + const store = TestBed.inject(MockStore) + store.overrideSelector(atlasSelection.selectors.navigation, currNavigation) + }) + describe("> when atlas is different", () => { + describe("> if no atlas prior", () => { + + it("> navigation should be reset", () => { + const effects = TestBed.inject(Effect) + const hook = effects.onTemplateParcSelectionPostHook[1] + const obs = hook({ + current: { + atlas: null, + parcellation: null, + template: null + }, + previous: { + atlas: { + "@id": IDS.ATLAES.RAT + } as any, + parcellation: { + "@id": IDS.PARCELLATION.WAXHOLMV4 + } as any, + template: { + "@id": IDS.TEMPLATES.WAXHOLM + } as any, + } + }) + + expect(obs).toBeObservable( + hot('(a|)', { + a: { + navigation: null + } + }) + ) + }) + }) + describe("> if different atlas prior", () => { + + it("> navigation should be reset", () => { + const effects = TestBed.inject(Effect) + const hook = effects.onTemplateParcSelectionPostHook[1] + const obs = hook({ + current: { + atlas: { + "@id": IDS.ATLAES.HUMAN + } as any, + parcellation: { + "@id": IDS.PARCELLATION.JBA29 + } as any, + template: { + "@id": IDS.TEMPLATES.MNI152 + } as any, + }, + previous: { + atlas: { + "@id": IDS.ATLAES.RAT + } as any, + parcellation: { + "@id": IDS.PARCELLATION.WAXHOLMV4 + } as any, + template: { + "@id": IDS.TEMPLATES.WAXHOLM + } as any, + } + }) + + expect(obs).toBeObservable( + hot('(a|)', { + a: { + navigation: null + } + }) + ) + }) + }) + }) + }) + }) describe('> if selected atlas has no matching tmpl space', () => { diff --git a/src/state/atlasSelection/effects.ts b/src/state/atlasSelection/effects.ts index 5461f7dd87b365d9e87028b87a10c69b9ad14342..2c95b75602df2ec483983d910f61ceccdcc7525d 100644 --- a/src/state/atlasSelection/effects.ts +++ b/src/state/atlasSelection/effects.ts @@ -52,6 +52,16 @@ export class Effect { ({ current, previous }) => { const prevSpcName = InterSpaceCoordXformSvc.TmplIdToValidSpaceName(previous?.template?.["@id"]) const currSpcName = InterSpaceCoordXformSvc.TmplIdToValidSpaceName(current?.template?.["@id"]) + + /** + * if trans-species, return default state for navigation + */ + if (previous?.atlas?.["@id"] !== current?.atlas?.["@id"]) { + return of({ + navigation: null + }) + } + /** * if either space name is undefined, return default state for navigation */ @@ -435,6 +445,5 @@ export class Effect { private store: Store, private interSpaceCoordXformSvc: InterSpaceCoordXformSvc, ){ - } } \ No newline at end of file diff --git a/src/state/atlasSelection/store.ts b/src/state/atlasSelection/store.ts index 08848c1efa9b81e649bfc8d7c29d3cc6581f77ed..ebea8a78dd3015d9b0b050c3902e40fb8ab2a0c8 100644 --- a/src/state/atlasSelection/store.ts +++ b/src/state/atlasSelection/store.ts @@ -117,9 +117,14 @@ const reducer = createReducer( on( actions.selectAtlas, (state, { atlas }) => { + if (atlas?.["@id"] === state?.selectedAtlas?.["@id"]) { + return state + } return { ...state, - selectedAtlas: atlas + selectedAtlas: atlas, + selectedTemplate: null, + selectedParcellation: null, } } ), diff --git a/src/state/plugins/actions.ts b/src/state/plugins/actions.ts index 5fe4fcd15b162f4bf26f5947d124e925dca43552..759c7523082503c38eaed07b9ff92f34b1b3e775 100644 --- a/src/state/plugins/actions.ts +++ b/src/state/plugins/actions.ts @@ -7,12 +7,3 @@ export const clearInitManifests = createAction( nameSpace: string }>() ) - -export const setInitMan = createAction( - `${nameSpace} setInitMan`, - props<{ - nameSpace: string - url: string - internal?: boolean - }>() -) diff --git a/src/state/plugins/effects.ts b/src/state/plugins/effects.ts index c27a74669d3dc0b96dd3e0506a41a14c5455220a..5f147b9b17f29011441b3c92fb7c92a992cca1a2 100644 --- a/src/state/plugins/effects.ts +++ b/src/state/plugins/effects.ts @@ -6,9 +6,8 @@ import * as constants from "./const" import * as selectors from "./selectors" import * as actions from "./actions" import { DialogService } from "src/services/dialogService.service"; -import { of } from "rxjs"; -import { HttpClient } from "@angular/common/http"; -import { getHttpHeader } from "src/util/constants" +import { NEVER, of } from "rxjs"; +import { PluginService } from "src/plugin/service"; @Injectable() export class Effects{ @@ -16,27 +15,33 @@ export class Effects{ initMan = this.store.pipe( select(selectors.initManfests), map(initMan => initMan[constants.INIT_MANIFEST_SRC]), - filter(val => !!val), + filter(val => val && val.length > 0), ) + private pendingList = new Set<string>() + private launchedList = new Set<string>() + private banList = new Set<string>() + initManLaunch = createEffect(() => this.initMan.pipe( - switchMap(val => - this.dialogSvc - .getUserConfirm({ - message: `This URL is trying to open a plugin from ${val}. Proceed?` - }) - .then(() => - this.http.get(val, { - headers: getHttpHeader(), - responseType: 'json' - }).toPromise() - ) - .then(json => { - /** - * TODO fix init plugin launch - * at that time, also restore effects.spec.ts test - */ - }) + switchMap(val => of(...val)), + switchMap( + url => { + if (this.pendingList.has(url)) return NEVER + if (this.launchedList.has(url)) return NEVER + if (this.banList.has(url)) return NEVER + this.pendingList.add(url) + return this.dialogSvc + .getUserConfirm({ + message: `This URL is trying to open a plugin from ${url}. Proceed?` + }) + .then(() => { + this.launchedList.add(url) + return this.svc.launchPlugin(url) + }) + .finally(() => { + this.pendingList.delete(url) + }) + } ), catchError(() => of(null)) ), { dispatch: false }) @@ -52,8 +57,8 @@ export class Effects{ constructor( private store: Store, private dialogSvc: DialogService, - private http: HttpClient, + private svc: PluginService, ){ } -} \ No newline at end of file +} diff --git a/src/state/plugins/store.ts b/src/state/plugins/store.ts index 83bb211ec912966efade6b361faced1dd43ede0c..7283a77a4f74431800af70c0360b0c8dfd2204f1 100644 --- a/src/state/plugins/store.ts +++ b/src/state/plugins/store.ts @@ -1,9 +1,8 @@ import { createReducer, on } from "@ngrx/store"; import * as actions from "./actions" -import { INIT_MANIFEST_SRC } from "./const" export type PluginStore = { - initManifests: Record<string, string> + initManifests: Record<string, string[]> } export const defaultState: PluginStore = { @@ -15,8 +14,8 @@ export const reducer = createReducer( on( actions.clearInitManifests, (state, { nameSpace }) => { - if (!state[nameSpace]) return state - const newMan: Record<string, string> = {} + if (!state.initManifests[nameSpace]) return state + const newMan: Record<string, string[]> = {} const { initManifests } = state for (const key in initManifests) { if (key === nameSpace) continue @@ -28,20 +27,4 @@ export const reducer = createReducer( } } ), - on( - actions.setInitMan, - (state, { nameSpace, url, internal }) => { - if (!internal) { - if (nameSpace === INIT_MANIFEST_SRC) return state - } - const { initManifests } = state - return { - ...state, - initManifests: { - ...initManifests, - [nameSpace]: url - } - } - } - ) ) diff --git a/src/strictLocal/index.ts b/src/strictLocal/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..df38eaa22fe65187cca51189b4442684509278ed --- /dev/null +++ b/src/strictLocal/index.ts @@ -0,0 +1,2 @@ +export { StrictLocalModule } from "./module" +export { HideWhenLocal } from "./strictLocal.directive" \ No newline at end of file diff --git a/src/strictLocal/module.ts b/src/strictLocal/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..62db4e8d888eb11107503bcb5b1ce8145b8a3d61 --- /dev/null +++ b/src/strictLocal/module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { HideWhenLocal } from "./strictLocal.directive"; +import { StrictLocalInfo } from "./strictLocalCmp/strictLocalCmp.component"; + +@NgModule({ + declarations: [ + HideWhenLocal, + StrictLocalInfo, + ], + imports: [ + CommonModule, + MatTooltipModule, + MatButtonModule, + ], + exports: [ + HideWhenLocal, + ] +}) + +export class StrictLocalModule{} diff --git a/src/strictLocal/strictLocal.directive.ts b/src/strictLocal/strictLocal.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5f0c83ad82310ec40b64906c3f6d5c266fb7a2f --- /dev/null +++ b/src/strictLocal/strictLocal.directive.ts @@ -0,0 +1,22 @@ +import { ComponentFactoryResolver, Directive, HostBinding, ViewContainerRef } from "@angular/core"; +import { environment } from "src/environments/environment" +import { StrictLocalInfo } from "./strictLocalCmp/strictLocalCmp.component"; + +@Directive({ + selector: '[sxplr-hide-when-local]', + exportAs: 'hideWhenLocal' +}) + +export class HideWhenLocal { + @HostBinding('style.display') + hideWhenLocal = environment.STRICT_LOCAL ? 'none!important' : null + constructor( + private vc: ViewContainerRef, + private cfr: ComponentFactoryResolver, + ){ + if (environment.STRICT_LOCAL) { + const cf = this.cfr.resolveComponentFactory(StrictLocalInfo) + this.vc.createComponent(cf) + } + } +} diff --git a/src/strictLocal/strictLocalCmp/strictLocalCmp.component.ts b/src/strictLocal/strictLocalCmp/strictLocalCmp.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e75b84c2b782a501996b66ca5a2be98687accda7 --- /dev/null +++ b/src/strictLocal/strictLocalCmp/strictLocalCmp.component.ts @@ -0,0 +1,13 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: `strict-local-info`, + template: ` + <button mat-icon-button [matTooltip]="tooltip" tabindex="-1"> + <i class="fas fa-unlink"></i> + </button>`, +}) + +export class StrictLocalInfo{ + tooltip = "External links are hidden in strict local mode." +} diff --git a/src/ui/dialogInfo/dialog.directive.ts b/src/ui/dialogInfo/dialog.directive.ts index 1b30c4e59ede96787318fe0f8582a2ee85317072..63d886a6ef5fa172ea1a8cf17abb5587005a5751 100644 --- a/src/ui/dialogInfo/dialog.directive.ts +++ b/src/ui/dialogInfo/dialog.directive.ts @@ -52,7 +52,7 @@ export class DialogDirective{ } this.matDialog.open(this.templateRef, { data: this.data, - ...sizeDict[this.size] + ...(sizeDict[this.size] || {}) }) } } \ No newline at end of file diff --git a/src/ui/help/about/about.template.html b/src/ui/help/about/about.template.html index 9a9169afd9eb25c1897e14b7dd950a5b55e4d212..380dbf4d2d83fbb0693857a00ac2929e13598905 100644 --- a/src/ui/help/about/about.template.html +++ b/src/ui/help/about/about.template.html @@ -1,7 +1,7 @@ <div class="container-fluid"> <div class="row mt-4 mb-4"> - <a [href]="userDoc" target="_blank"> + <a sxplr-hide-when-local [href]="userDoc" target="_blank"> <button mat-raised-button color="primary"> <i class="fas fa-book-open"></i> <span> @@ -10,7 +10,7 @@ </button> </a> - <a [href]="repoUrl" target="_blank"> + <a sxplr-hide-when-local [href]="repoUrl" target="_blank"> <button mat-flat-button> <i class="fab fa-github"></i> <span> diff --git a/src/ui/help/module.ts b/src/ui/help/module.ts index b99786401f97aad174c2e5db0398223d377be5c7..5dded09484db45d2ff96e81ac3502f11ae69f737 100644 --- a/src/ui/help/module.ts +++ b/src/ui/help/module.ts @@ -7,6 +7,7 @@ import { AboutCmp } from './about/about.component' import { HelpOnePager } from "./helpOnePager/helpOnePager.component"; import {QuickTourModule} from "src/ui/quickTour/module"; import { HowToCite } from "./howToCite/howToCite.component"; +import { StrictLocalModule } from "src/strictLocal"; @NgModule({ imports: [ @@ -14,7 +15,8 @@ import { HowToCite } from "./howToCite/howToCite.component"; AngularMaterialModule, ComponentsModule, UtilModule, - QuickTourModule + QuickTourModule, + StrictLocalModule, ], declarations: [ AboutCmp, diff --git a/src/ui/quickTour/quickTour.service.ts b/src/ui/quickTour/quickTour.service.ts index 3853a19497de7f3494d77c587b1a1e044cf00f5c..3aed96edf8d1cde0ff32642954be57f735a634a5 100644 --- a/src/ui/quickTour/quickTour.service.ts +++ b/src/ui/quickTour/quickTour.service.ts @@ -90,7 +90,7 @@ export class QuickTourService { height: '0px', width: '0px', hasBackdrop: true, - backdropClass: ['pe-none', 'cdk-overlay-dark-backdrop'], + backdropClass: ['sxplr-pe-none', 'cdk-overlay-dark-backdrop'], positionStrategy: this.overlay.position().global(), }) } diff --git a/src/util/constants.ts b/src/util/constants.ts index 5c4e7185743a1dca35e0523f076385b797fc21b5..7879151a46014b4576f159e55d118cce75aa748c 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -32,9 +32,12 @@ export const MIN_REQ_EXPLAINER = ` export const APPEND_SCRIPT_TOKEN: InjectionToken<(url: string) => Promise<HTMLScriptElement>> = new InjectionToken(`APPEND_SCRIPT_TOKEN`) -export const appendScriptFactory = (document: Document) => { +export const appendScriptFactory = (document: Document, defer: boolean = false) => { return src => new Promise((rs, rj) => { const scriptEl = document.createElement('script') + if (defer) { + scriptEl.defer = true + } scriptEl.src = src scriptEl.onload = () => rs(scriptEl) scriptEl.onerror = (e) => rj(e) @@ -115,7 +118,6 @@ export const compareLandmarksChanged: (prevLandmarks: any[], newLandmarks: any[] } export const CYCLE_PANEL_MESSAGE = `[spacebar] to cycle through views` -export const BS_ENDPOINT = new InjectionToken<string>('BS_ENDPOINT') export const UNSUPPORTED_PREVIEW = [{ text: 'Preview of Colin 27 and JuBrain Cytoarchitectonic', diff --git a/src/util/priority.ts b/src/util/priority.ts index ab703f76b14e1a299fa2ae017d81038a43c1c858..79ef1b043e4d5bb13a99e157995315e05e94b8d5 100644 --- a/src/util/priority.ts +++ b/src/util/priority.ts @@ -24,6 +24,8 @@ type Queue = { next: HttpHandler } +export const DISABLE_PRIORITY_HEADER = 'x-sxplr-disable-priority' + @Injectable({ providedIn: 'root' }) @@ -137,8 +139,11 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ * Since the way in which serialization occurs is via path and query param... * body is not used. */ - if (this.disablePriority || req.method !== 'GET') { - return next.handle(req) + if (this.disablePriority || req.method !== 'GET' || !!req.headers.get(DISABLE_PRIORITY_HEADER)) { + const newReq = req.clone({ + headers: req.headers.delete(DISABLE_PRIORITY_HEADER) + }) + return next.handle(newReq) } const { urlWithParams } = req diff --git a/src/viewerModule/leap/index.ts b/src/viewerModule/leap/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/viewerModule/leap/leapSignal/leapSignal.component.ts b/src/viewerModule/leap/leapSignal/leapSignal.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..046998972b022e782e14199028fe144160174363 --- /dev/null +++ b/src/viewerModule/leap/leapSignal/leapSignal.component.ts @@ -0,0 +1,40 @@ +import { Component } from "@angular/core"; +import { map } from "rxjs/operators"; +import { LeapService } from "../service"; +import { HandShape } from "../service" + +@Component({ + selector: 'leap-control-signal', + templateUrl: './leapSignal.template.html', + styleUrls: [ + './leapSignal.style.css' + ] +}) +export class LeapSignal{ + public ready$ = this.svc.leapReady$ + public handDetected$ = this.svc.hand$.pipe( + map(hands => hands && hands.length > 0) + ) + public gestureText$ = this.svc.gesture$.pipe( + map(gesture => { + if (gesture === HandShape.PINCHING) { + return "Pinching" + } + if (gesture === HandShape.PALM_FORWARD) { + return "Zooming" + } + if (gesture === HandShape.POINTING_ONE_FINGER) { + return "Translating" + } + if (gesture === HandShape.POINTING_TWO_FINGERS) { + return "Oblique Cutting" + } + return null + }) + ) + constructor( + private svc: LeapService + ){ + + } +} diff --git a/src/viewerModule/leap/leapSignal/leapSignal.style.css b/src/viewerModule/leap/leapSignal/leapSignal.style.css new file mode 100644 index 0000000000000000000000000000000000000000..1ca31c86031d27db3c0013d09af57a1e74cacf9c --- /dev/null +++ b/src/viewerModule/leap/leapSignal/leapSignal.style.css @@ -0,0 +1,19 @@ +.leap-spinner +{ + animation: spinning 1400ms linear infinite running; +} + +@keyframes spinning +{ + from { + transform: rotate(0deg); + } + to{ + transform: rotate(359deg); + } +} + +.leap-muted +{ + opacity: 0.5; +} diff --git a/src/viewerModule/leap/leapSignal/leapSignal.template.html b/src/viewerModule/leap/leapSignal/leapSignal.template.html new file mode 100644 index 0000000000000000000000000000000000000000..3a0c1dfb42086f2a47c227572129d61db50187f2 --- /dev/null +++ b/src/viewerModule/leap/leapSignal/leapSignal.template.html @@ -0,0 +1,30 @@ +<ng-template [ngIf]="ready$ | async"> + <ng-template [ngTemplateOutlet]="leapCtrlTmpl"></ng-template> +</ng-template> + + +<ng-template #leapCtrlTmpl> + + <mat-card> + <span *ngIf="gestureText$ | async as text"> + {{ text }} + </span> + <!-- detecting hand --> + <ng-template [ngTemplateOutlet]="(handDetected$ | async) ? handFound : noHandFound"> + </ng-template> + </mat-card> + + <ng-template #handFound> + <span class="fa-stack fa-2x"> + <i class="fas fa-hand-paper fa-stack-1x"></i> + </span> + </ng-template> + + <ng-template #noHandFound> + <span class="fa-stack fa-2x"> + <i class="leap-muted fas fa-hand-paper fa-stack-1x"></i> + <i class="leap-muted leap-spinner fas fa-circle-notch fa-stack-2x"></i> + </span> + </ng-template> + +</ng-template> diff --git a/src/viewerModule/leap/module.ts b/src/viewerModule/leap/module.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e7bcbfc54c984cf6bdfa87aa1aeab57ed40f8b2 --- /dev/null +++ b/src/viewerModule/leap/module.ts @@ -0,0 +1,36 @@ +import { CommonModule } from "@angular/common"; +import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { MatCardModule } from "@angular/material/card"; +import { APPEND_SCRIPT_TOKEN } from "src/util/constants"; +import { LeapSignal } from "./leapSignal/leapSignal.component"; +import { LeapService } from "./service"; +import { LeapControlViewRef } from "./signal.directive"; + +@NgModule({ + imports: [ + CommonModule, + MatCardModule, + ], + declarations: [ + LeapSignal, + LeapControlViewRef, + ], + exports: [ + LeapSignal, + LeapControlViewRef, + ], + providers: [ + LeapService, + { + provide: APP_INITIALIZER, + useFactory(appendSrc: (src: string, deferFlag?: boolean) => Promise<void>){ + return () => appendSrc("leap-0.6.4.js", true) + }, + deps: [ + APPEND_SCRIPT_TOKEN + ], + multi: true + }, + ], +}) +export class LeapModule{} \ No newline at end of file diff --git a/src/viewerModule/leap/service.ts b/src/viewerModule/leap/service.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ce47c14dccf14a36431e26153d4db723ccbf365 --- /dev/null +++ b/src/viewerModule/leap/service.ts @@ -0,0 +1,252 @@ +import { DOCUMENT } from "@angular/common"; +import { Inject, Injectable, Optional } from "@angular/core"; +import { BehaviorSubject, EMPTY, interval, Observable, Subject } from "rxjs"; +import { distinctUntilChanged, filter, map, switchMap, take, takeUntil } from "rxjs/operators"; +import { NehubaViewerUnit } from "../nehuba"; +import { NEHUBA_INSTANCE_INJTKN } from "../nehuba/util"; + +const PINCH_THRESHOLD = 0.80 +const PALM_Z_THRESHOLD = 0.55 +const ROTATION_SPEED = 0.0001 +const ZOOM_SPEED = 40 + +type Finger = { + extended: boolean +} + +type Hand = { + pinchStrength: number + palmNormal: [number, number, number] + palmVelocity: [number, number, number] + + thumb: Finger + indexFinger: Finger + middleFinger: Finger + ringFinger: Finger + pinky: Finger +} + +export enum HandShape { + PINCHING="PINCHING", + POINTING_ONE_FINGER="POINTING_ONE_FINGER", + POINTING_TWO_FINGERS="POINTING_TWO_FINGERS", + PALM_FORWARD="PALM_FORWARD", +} + +function getHandShape(hand: Hand): HandShape { + if (!hand) return null + if (hand.pinchStrength >= PINCH_THRESHOLD) return HandShape.PINCHING + if ( + hand.thumb.extended && + hand.indexFinger.extended && + !hand.middleFinger.extended && + !hand.ringFinger.extended && + !hand.pinky.extended + ) return HandShape.POINTING_ONE_FINGER + if ( + hand.thumb.extended && + hand.indexFinger.extended && + hand.middleFinger.extended && + !hand.ringFinger.extended && + !hand.pinky.extended + ) return HandShape.POINTING_TWO_FINGERS + + const palmNormalZ = hand.palmNormal[2] + if ( + hand.thumb.extended && + hand.indexFinger.extended && + hand.middleFinger.extended && + hand.ringFinger.extended && + hand.pinky.extended && + palmNormalZ <= -PALM_Z_THRESHOLD + ) return HandShape.PALM_FORWARD + return null +} + +@Injectable() +export class LeapService{ + static initFlag = false + + private destroy$ = new Subject() + + public leapReady$ = new BehaviorSubject(false) + + public hand$ = new Subject<Hand[]>() + public gesture$ = new Subject<HandShape>() + public palmVelocity$ = new Subject<[number, number, number]>() + + private sliceLock = false + private vec3: any + private quat: any + + private rotation: any + private WORLD_UP: any + private WORLD_RIGHT: any + + constructor( + @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaInst$: Observable<NehubaViewerUnit>, + @Inject(DOCUMENT) document: Document, + ){ + if (LeapService.initFlag) { + console.error(`LeapService already initialised.`) + } + if (!(window as any).Leap) { + console.error(`Leap not found. Terminating`) + return + } + const lop = (window as any).Leap.loop({ + frame: frame => { + this.leapReady$.next(true) + this.hand$.next(frame?.hands) + if (frame?.hands) { + this.emitHand(frame.hands[0]) + } + } + }) + + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "hidden") { + lop.connection.disconnect() + } else { + lop.connection.reconnect() + } + }) + + interval(160).pipe( + filter(() => !!(window as any).export_nehuba), + take(1) + ).subscribe(() => { + this.vec3 = (window as any).export_nehuba.vec3 + this.quat = (window as any).export_nehuba.quat + + this.rotation = this.quat.create() + this.WORLD_UP = this.vec3.fromValues(0, 1, 0) + this.WORLD_RIGHT = this.vec3.fromValues(1, 0, 0) + }) + + this.nehubaInst$.pipe( + switchMap(nehuba => { + if (!nehuba) return EMPTY + return this.gesture$.pipe( + distinctUntilChanged(), + switchMap(gesture => { + if (!gesture) return EMPTY + return this.palmVelocity$.pipe( + map(velocity => { + return { + nehuba, + gesture, + velocity + } + }) + ) + }) + ) + }), + takeUntil(this.destroy$) + ).subscribe(({ gesture, nehuba, velocity }) => { + if (gesture === HandShape.PINCHING) { + const vel = velocity + const temp = vel[1] + vel[1] = -vel[2] + vel[2] = temp + this.leapToRotation( + vel, + ROTATION_SPEED * 4, + nehuba.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation + ) + return + } + + if (gesture === HandShape.PALM_FORWARD) { + /** + * zooming + */ + const vel = -velocity[2] + if (nehuba.nehubaViewer.ngviewer.navigationState.zoomFactor.value >= 10000) { + nehuba.nehubaViewer.ngviewer.navigationState.zoomFactor.value += ZOOM_SPEED * vel; + } else { + nehuba.nehubaViewer.ngviewer.navigationState.zoomFactor.value = 10001; + } + return + } + + if (gesture === HandShape.POINTING_ONE_FINGER) { + /** + * translating + */ + const SPEED_SCALE = 5 * 8000 * (nehuba.nehubaViewer.ngviewer.navigationState.zoomFactor.value / 400000) + const vel = velocity + vel[1] = -vel[1] + vel[2] = -vel[2] + const cur = nehuba.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation.orientation + this.vec3.transformQuat(vel, vel, cur) + const { position } = nehuba.nehubaViewer.ngviewer.navigationState.pose + this.vec3.scaleAndAdd( + position.spatialCoordinates, + position.spatialCoordinates, + vel, + SPEED_SCALE + ) + position.changed.dispatch(); + } + + if (gesture === HandShape.POINTING_TWO_FINGERS) { + const ROTATION_SPEED = 0.0004; + if (!this.sliceLock) { + const vel = velocity + const temp = vel[1] + vel[0] = -vel[0] + vel[1] = vel[2] + vel[2] = -temp + this.leapToRotation( + vel, + ROTATION_SPEED, + nehuba.nehubaViewer.ngviewer.navigationState.pose.orientation, + nehuba.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation.orientation + ) + } + } + }) + + LeapService.initFlag = true + } + + private leapToRotation(vel: [number, number, number], rotSpeed: number, orientationStream: any, camOrientation = null){ + let cur = orientationStream.orientation; + const axis = this.vec3.create() + if (camOrientation) { + this.vec3.transformQuat(axis, this.WORLD_UP, camOrientation); + this.vec3.transformQuat(axis, axis, cur); + } else { + this.vec3.transformQuat(axis, this.WORLD_UP, cur); + } + this.quat.setAxisAngle(this.rotation, axis, vel[0] * rotSpeed); + this.quat.multiply(orientationStream.orientation, this.rotation, cur); + cur = orientationStream.orientation; + if (camOrientation) { + this.vec3.transformQuat(axis, this.WORLD_RIGHT, camOrientation); + this.vec3.transformQuat(axis, axis, cur); + } else { + this.vec3.transformQuat(axis, this.WORLD_RIGHT, cur); + } + this.quat.setAxisAngle(this.rotation, axis, vel[2] * rotSpeed); + this.quat.multiply(orientationStream.orientation, this.rotation, cur); + orientationStream.changed.dispatch(); + } + + private emitHand(hand: Hand){ + let handShape: HandShape + try{ + handShape = getHandShape(hand) + this.gesture$.next( + handShape + ) + this.palmVelocity$.next( + hand?.palmVelocity + ) + } catch (e) { + console.error(e) + } + } +} diff --git a/src/viewerModule/leap/signal.directive.ts b/src/viewerModule/leap/signal.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..4add98f470090b81aeabd7a9ef90ec54cd562dde --- /dev/null +++ b/src/viewerModule/leap/signal.directive.ts @@ -0,0 +1,15 @@ +import { ComponentFactoryResolver, Directive, ViewContainerRef } from "@angular/core"; +import { LeapSignal } from "./leapSignal/leapSignal.component"; + +@Directive({ + selector: '[leap-control-view-ref]' +}) +export class LeapControlViewRef { + constructor( + private vcr: ViewContainerRef, + private cfr: ComponentFactoryResolver, + ){ + const cf = this.cfr.resolveComponentFactory(LeapSignal) + this.vcr.createComponent(cf) + } +} diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts index 14779f2a3f322e0162e88c35a615adf78482aec7..e7107258f1c109ddb6f49266dc710c326ef4cbf9 100644 --- a/src/viewerModule/module.ts +++ b/src/viewerModule/module.ts @@ -26,6 +26,9 @@ import { MouseoverModule } from "src/mouseoverModule"; import { LogoContainer } from "src/ui/logoContainer/logoContainer.component"; import { FloatingMouseContextualContainerDirective } from "src/util/directives/floatingMouseContextualContainer.directive"; import { ShareModule } from "src/share"; +import { LeapModule } from "./leap/module"; + +import { environment } from "src/environments/environment" @NgModule({ imports: [ @@ -47,6 +50,7 @@ import { ShareModule } from "src/share"; DialogModule, MouseoverModule, ShareModule, + ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : []) ], declarations: [ ViewerCmp, diff --git a/src/viewerModule/nehuba/config.service/util.ts b/src/viewerModule/nehuba/config.service/util.ts index 7233d0f26bb2bda42bfe3b0141c9c9b64a377c77..8f2b0de2dc144f287815a503dd95e35c741e82fb 100644 --- a/src/viewerModule/nehuba/config.service/util.ts +++ b/src/viewerModule/nehuba/config.service/util.ts @@ -409,6 +409,11 @@ export function getNehubaConfig(space: SapiSpaceModel): NehubaConfig { "layers": {}, "navigation": { "zoomFactor": 350000 * scale, + pose: { + position: { + voxelSize: [1, 1, 1] + } + } }, "perspectiveOrientation": [ 0.3140767216682434, diff --git a/src/viewerModule/nehuba/constants.ts b/src/viewerModule/nehuba/constants.ts index 6537a7aef4ea762d6d38c85ac335011551b5767e..ca5ae4be810af998f93318a6348f751555fabff3 100644 --- a/src/viewerModule/nehuba/constants.ts +++ b/src/viewerModule/nehuba/constants.ts @@ -64,3 +64,5 @@ export interface IMeshesToLoad { } export const SET_MESHES_TO_LOAD = new InjectionToken<Observable<IMeshesToLoad>>('SET_MESHES_TO_LOAD') + +export const PMAP_LAYER_NAME = 'regional-pmap' diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts index 6f6d708876773bd84a484d73226fab01066f731a..6886634b6e6c02a6556dcaa8a18c3f4729b7b71a 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts @@ -11,7 +11,7 @@ import { EnumColorMapName } from "src/util/colorMaps"; import { getShader } from "src/util/constants"; import { getNgLayersFromVolumesATP, getRegionLabelIndex } from "../config.service"; import { ParcVolumeSpec } from "../store/util"; -import { NehubaLayerControlService } from "./layerCtrl.service"; +import { PMAP_LAYER_NAME } from "../constants"; @Injectable() export class LayerCtrlEffects { @@ -22,7 +22,7 @@ export class LayerCtrlEffects { ), mapTo( atlasAppearance.actions.removeCustomLayer({ - id: NehubaLayerControlService.PMAP_LAYER_NAME + id: PMAP_LAYER_NAME }) ) )) @@ -37,17 +37,20 @@ export class LayerCtrlEffects { ), switchMap(([ regions, { atlas, parcellation, template } ]) => { const sapiRegion = this.sapi.getRegion(atlas["@id"], parcellation["@id"], regions[0].name) - return sapiRegion.getMapInfo(template["@id"]).pipe( - map(val => + return forkJoin([ + sapiRegion.getMapInfo(template["@id"]), + sapiRegion.getMapUrl(template["@id"]) + ]).pipe( + map(([mapInfo, mapUrl]) => atlasAppearance.actions.addCustomLayer({ customLayer: { clType: "customlayer/nglayer", - id: NehubaLayerControlService.PMAP_LAYER_NAME, - source: `nifti://${sapiRegion.getMapUrl(template["@id"])}`, + id: PMAP_LAYER_NAME, + source: `nifti://${mapUrl}`, shader: getShader({ colormap: EnumColorMapName.VIRIDIS, - highThreshold: val.max, - lowThreshold: val.min, + highThreshold: mapInfo.max, + lowThreshold: mapInfo.min, removeBg: true, }) } @@ -55,7 +58,7 @@ export class LayerCtrlEffects { ), catchError(() => of( atlasAppearance.actions.removeCustomLayer({ - id: NehubaLayerControlService.PMAP_LAYER_NAME + id: PMAP_LAYER_NAME }) )) ) diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts index ba8b1d0adbe40c1c00fb794cac66a0b1890c25ae..da9855ecb93b0344c2239776590f0625dd20117a 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts @@ -13,6 +13,9 @@ import { arrayEqual } from "src/util/array"; import { ColorMapCustomLayer } from "src/state/atlasAppearance"; import { SapiRegionModel } from "src/atlasComponents/sapi"; import { AnnotationLayer } from "src/atlasComponents/annotations"; +import { PMAP_LAYER_NAME } from "../constants" +import { EnumColorMapName, mapKeyColorMap } from "src/util/colorMaps"; +import { getShader } from "src/util/constants"; export const BACKUP_COLOR = { red: 255, @@ -25,8 +28,6 @@ export const BACKUP_COLOR = { }) export class NehubaLayerControlService implements OnDestroy{ - static PMAP_LAYER_NAME = 'regional-pmap' - private selectedRegion$ = this.store$.pipe( select(atlasSelection.selectors.selectedRegions), shareReplay(1), @@ -175,14 +176,23 @@ export class NehubaLayerControlService implements OnDestroy{ * on custom landmarks loaded, set mesh transparency */ this.sub.push( - this.store$.pipe( - select(annotation.selectors.annotations), + merge( + this.store$.pipe( + select(annotation.selectors.annotations), + map(landmarks => landmarks.length > 0), + ), + this.store$.pipe( + select(atlasAppearance.selectors.customLayers), + map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer" && /^swc:\/\//.test(l.source)).length > 0), + ) + ).pipe( + startWith(false), withLatestFrom(this.defaultNgLayers$) - ).subscribe(([landmarks, { tmplAuxNgLayers }]) => { + ).subscribe(([flag, { tmplAuxNgLayers }]) => { const payload: { [key: string]: number } = {} - const alpha = landmarks.length > 0 + const alpha = flag ? 0.2 : 1.0 for (const ngId in tmplAuxNgLayers) { @@ -276,15 +286,20 @@ export class NehubaLayerControlService implements OnDestroy{ private ngLayersRegister: atlasAppearance.NgLayerCustomLayer[] = [] - private updateCustomLayerTransparency$ = this.store$.pipe( - select(atlasAppearance.selectors.customLayers), - map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]), - pairwise(), - map(([ oldCustomLayers, newCustomLayers ]) => { - return newCustomLayers.filter(({ id, opacity }) => oldCustomLayers.some(({ id: oldId, opacity: oldOpacity }) => oldId === id && oldOpacity !== opacity)) - }), - filter(arr => arr.length > 0) - ) + private getUpdatedCustomLayer(isSameLayer: (o: atlasAppearance.NgLayerCustomLayer, n: atlasAppearance.NgLayerCustomLayer) => boolean){ + return this.store$.pipe( + select(atlasAppearance.selectors.customLayers), + map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]), + pairwise(), + map(([ oldCustomLayers, newCustomLayers ]) => { + return newCustomLayers.filter(n => oldCustomLayers.some(o => o.id === n.id && !isSameLayer(o, n))) + }), + filter(arr => arr.length > 0), + ) + } + + private updateCustomLayerTransparency$ = this.getUpdatedCustomLayer((o, n) => o.opacity === n.opacity) + private updateCustomLayerColorMap$ = this.getUpdatedCustomLayer((o, n) => o.shader === n.shader) private ngLayers$ = this.customLayers$.pipe( map(customLayers => customLayers.filter(l => l.clType === "customlayer/nglayer") as atlasAppearance.NgLayerCustomLayer[]), @@ -347,6 +362,19 @@ export class NehubaLayerControlService implements OnDestroy{ } as TNgLayerCtrl<'setLayerTransparency'> }) ), + this.updateCustomLayerColorMap$.pipe( + map(layers => { + const payload: Record<string, string> = {} + for (const layer of layers) { + const shader = layer.shader ?? getShader() + payload[layer.id] = shader + } + return { + type: 'updateShader', + payload + } as TNgLayerCtrl<'updateShader'> + }) + ), this.manualNgLayersControl$, ).pipe( ) @@ -367,7 +395,7 @@ export class NehubaLayerControlService implements OnDestroy{ */ return customLayers .map(l => l.id) - .filter(name => name !== NehubaLayerControlService.PMAP_LAYER_NAME) + .filter(name => name !== PMAP_LAYER_NAME) }) ), this.customLayers$.pipe( @@ -378,7 +406,7 @@ export class NehubaLayerControlService implements OnDestroy{ }), distinctUntilChanged(), map(flag => flag - ? [ NehubaLayerControlService.PMAP_LAYER_NAME ] + ? [ PMAP_LAYER_NAME ] : [] ) ) diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts index d5a743d67ee5dfe7ab13521b7ae0383d2e9a7524..1fa9f52ff34373fdb76dd62ddeb7acffc71ff091 100644 --- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts +++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts @@ -51,6 +51,9 @@ export interface INgLayerCtrl { setLayerTransparency: { [key: string]: number } + updateShader: { + [key: string]: string + } } export type TNgLayerCtrl<T extends keyof INgLayerCtrl> = { diff --git a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts index a5f13432b611c3fee0b082338df7c3e3b086cf7d..2fcf3b824328773660a44e725872f1aafd36dfd5 100644 --- a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts +++ b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, ChangeDetectorRef, Component, Inject, OnDestroy } from "@angular/core"; +import { ChangeDetectorRef, Component, Inject, OnDestroy } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { combineLatest, fromEvent, interval, merge, Observable, of, Subject, Subscription } from "rxjs"; import { userInterface } from "src/state"; @@ -6,7 +6,7 @@ import { NehubaViewerUnit } from "../../nehubaViewer/nehubaViewer.component"; import { NEHUBA_INSTANCE_INJTKN, takeOnePipe, getFourPanel, getHorizontalOneThree, getSinglePanel, getVerticalOneThree } from "../../util"; import { QUICKTOUR_DESC, ARIA_LABELS, IDS } from 'common/constants' import { IQuickTourData } from "src/ui/quickTour/constrants"; -import { debounce, debounceTime, filter, mapTo, switchMap, take } from "rxjs/operators"; +import { debounce, debounceTime, distinctUntilChanged, filter, map, mapTo, switchMap, take } from "rxjs/operators"; @Component({ selector: `nehuba-layout-overlay`, @@ -16,7 +16,7 @@ import { debounce, debounceTime, filter, mapTo, switchMap, take } from "rxjs/ope ] }) -export class NehubaLayoutOverlay implements OnDestroy, AfterViewInit{ +export class NehubaLayoutOverlay implements OnDestroy{ public ARIA_LABELS = ARIA_LABELS public IDS = IDS @@ -44,10 +44,6 @@ export class NehubaLayoutOverlay implements OnDestroy, AfterViewInit{ while(this.nehubaUnitSubs.length > 0) this.nehubaUnitSubs.pop().unsubscribe() } - ngAfterViewInit(): void { - this.setQuickTourPos() - } - handleCycleViewEvent(): void { if (this.currentPanelMode !== "SINGLE_PANEL") return this.store$.dispatch( @@ -124,6 +120,7 @@ export class NehubaLayoutOverlay implements OnDestroy, AfterViewInit{ nehuba$.subscribe(nehuba => { this.nehubaUnit = nehuba this.onNewNehubaUnit(nehuba) + this.setQuickTourPos() }) ) } @@ -154,11 +151,17 @@ export class NehubaLayoutOverlay implements OnDestroy, AfterViewInit{ fromEvent<CustomEvent>( nehubaUnit.elementRef.nativeElement, 'sliceRenderEvent' - ).subscribe(ev => { - const { missingImageChunks, missingChunks } = ev.detail + ).pipe( + map(ev => { + const { missingImageChunks, missingChunks } = ev.detail + return { missingImageChunks, missingChunks } + }), + distinctUntilChanged((o, n) => o.missingChunks === n.missingChunks && o.missingImageChunks === n.missingImageChunks) + ).subscribe(({ missingImageChunks, missingChunks }) => { this.volumeChunkLoading$.next( - missingImageChunks.length === 0 && missingChunks.length === 0 + missingImageChunks > 0 || missingChunks > 0 ) + this.detectChanges() }), /** diff --git a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html index e716c29fac236a4c7394b76a68a5027ffaf94646..245fb9f07ed2da09550b1bc63a6f1d9497a924cb 100644 --- a/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html +++ b/src/viewerModule/nehuba/layoutOverlay/nehuba.layoutOverlay/nehuba.layoutOverlay.template.html @@ -68,7 +68,7 @@ [attr.data-viewer-controller-visible]="visible" [attr.data-viewer-controller-index]="panelIndex"> - <div class="position-absolute w-100 h-100 pe-none" + <div class="position-absolute w-100 h-100 sxplr-pe-none" *ngIf="panelIndex === 1" quick-tour [quick-tour-description]="quickTourIconsSlide.description" diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts index 62a4ce09eb3d14682498812aee8e9e90dd01b253..ef3ac53320fbfa6ee86ec80ac140c105a2534af8 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.spec.ts @@ -7,7 +7,7 @@ import { SapiRegionModel } from "src/atlasComponents/sapi" import * as configSvc from "../config.service" import { LayerCtrlEffects } from "../layerCtrl.service/layerCtrl.effects" import { NEVER, of, pipe } from "rxjs" -import { mapTo } from "rxjs/operators" +import { mapTo, take } from "rxjs/operators" import { selectorAuxMeshes } from "../store" @@ -51,6 +51,12 @@ describe('> mesh.service.ts', () => { ) ) }) + + afterEach(() => { + getParcNgIdSpy.calls.reset() + getRegionLabelIndexSpy.calls.reset() + getATPSpy.calls.reset() + }) describe('> NehubaMeshService', () => { beforeEach(() => { TestBed.configureTestingModule({ @@ -72,37 +78,106 @@ describe('> mesh.service.ts', () => { expect(service).toBeTruthy() }) - it('> mixes in auxillaryMeshIndices', () => { - const mockStore = TestBed.inject(MockStore) - mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ]) - mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, []) - mockStore.overrideSelector(selectorAuxMeshes, [auxMesh]) + describe("> loadMeshes$", () => { - const ngId = 'blabla' - const labelIndex = 12 - getParcNgIdSpy.and.returnValue(ngId) - getRegionLabelIndexSpy.and.returnValue(labelIndex) + describe("> auxMesh defined", () => { + + const ngId = 'blabla' + const labelIndex = 12 + + beforeEach(() => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ]) + mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, []) + mockStore.overrideSelector(selectorAuxMeshes, [auxMesh]) + + getParcNgIdSpy.and.returnValue(ngId) + getRegionLabelIndexSpy.and.returnValue(labelIndex) - const service = TestBed.inject(NehubaMeshService) - - expect( - service.loadMeshes$ - ).toBeObservable( - hot('(ab)', { - a: { - layer: { - name: ngId - }, - labelIndicies: [ labelIndex ] - }, - b: { - layer: { - name: auxMesh.ngId, - }, - labelIndicies: auxMesh.labelIndicies - } }) - ) + + it("> auxMesh ngId labelIndex emitted", () => { + + const service = TestBed.inject(NehubaMeshService) + expect( + service.loadMeshes$ + ).toBeObservable( + hot('(ab)', { + a: { + layer: { + name: ngId + }, + labelIndicies: [ labelIndex ] + }, + b: { + layer: { + name: auxMesh.ngId, + }, + labelIndicies: auxMesh.labelIndicies + } + }) + ) + }) + }) + + describe("> if multiple ngid and labelindicies are present", () => { + + const ngId1 = 'blabla' + const labelIndex1 = 12 + + const ngId2 = 'foobar' + const labelIndex2 = 13 + + beforeEach(() => { + + const mockStore = TestBed.inject(MockStore) + mockStore.overrideSelector(atlasSelection.selectors.selectedRegions, [ fits1 ]) + mockStore.overrideSelector(atlasSelection.selectors.selectedParcAllRegions, [fits1, fits1]) + mockStore.overrideSelector(selectorAuxMeshes, []) + + getParcNgIdSpy.and.returnValues(ngId1, ngId2, ngId2) + getRegionLabelIndexSpy.and.returnValues(labelIndex1, labelIndex2, labelIndex2) + }) + + it('> should call getParcNgIdSpy and getRegionLabelIndexSpy thrice', () => { + const service = TestBed.inject(NehubaMeshService) + service.loadMeshes$.pipe( + take(1) + ).subscribe(() => { + + expect(getParcNgIdSpy).toHaveBeenCalledTimes(3) + expect(getRegionLabelIndexSpy).toHaveBeenCalledTimes(3) + }) + }) + + /** + * in the case of julich brain 2.9 in colin 27, we expect selecting a region will hide meshes from all relevant ngIds (both left and right) + */ + it('> expect the emitted value to be incl all ngIds', () => { + const service = TestBed.inject(NehubaMeshService) + expect( + service.loadMeshes$ + ).toBeObservable( + hot('(ab)', { + a: { + layer: { + name: ngId1 + }, + labelIndicies: [] + }, + b: { + layer: { + name: ngId2 + }, + labelIndicies: [ labelIndex2 ] + } + }) + ) + + }) + }) + }) }) }) diff --git a/src/viewerModule/nehuba/mesh.service/mesh.service.ts b/src/viewerModule/nehuba/mesh.service/mesh.service.ts index 2585f00224e8729a4434afa568e127a486e66fcb..d372ce460746d01c5cf560518e1532f38616b882 100644 --- a/src/viewerModule/nehuba/mesh.service/mesh.service.ts +++ b/src/viewerModule/nehuba/mesh.service/mesh.service.ts @@ -47,7 +47,40 @@ export class NehubaMeshService implements OnDestroy { ]).pipe( switchMap(([{ atlas, template, parcellation }, regions, selectedRegions]) => { const ngIdRecord: Record<string, number[]> = {} + + const tree = new Tree( + regions, + (c, p) => (c.hasParent || []).some(_p => _p["@id"] === p["@id"]) + ) + + for (const r of regions) { + const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r ) + if (!regionLabelIndex) { + continue + } + if ( + tree.someAncestor(r, anc => !!getRegionLabelIndex(atlas, template, parcellation, anc)) + ) { + continue + } + const ngId = getParcNgId(atlas, template, parcellation, r) + if (!ngIdRecord[ngId]) { + ngIdRecord[ngId] = [] + } + ngIdRecord[ngId].push(regionLabelIndex) + } + if (selectedRegions.length > 0) { + /** + * If regions are selected, reset the meshes + */ + for (const key in ngIdRecord) { + ngIdRecord[key] = [] + } + + /** + * only show selected region + */ for (const r of selectedRegions) { const ngId = getParcNgId(atlas, template, parcellation, r) const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r ) @@ -56,28 +89,6 @@ export class NehubaMeshService implements OnDestroy { } ngIdRecord[ngId].push(regionLabelIndex) } - } else { - const tree = new Tree( - regions, - (c, p) => (c.hasParent || []).some(_p => _p["@id"] === p["@id"]) - ) - - for (const r of regions) { - const regionLabelIndex = getRegionLabelIndex( atlas, template, parcellation, r ) - if (!regionLabelIndex) { - continue - } - if ( - tree.someAncestor(r, (anc) => !!getRegionLabelIndex(atlas, template, parcellation, anc)) - ) { - continue - } - const ngId = getParcNgId(atlas, template, parcellation, r) - if (!ngIdRecord[ngId]) { - ngIdRecord[ngId] = [] - } - ngIdRecord[ngId].push(regionLabelIndex) - } } const arr: IMeshesToLoad[] = [] diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts index c6437fc7c93844af930c70ce2c5ae019227f76cf..6c5bb5aeb0c02634389135fe0056c5b88644d89b 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.spec.ts @@ -1,7 +1,6 @@ import { TestBed, fakeAsync, tick, ComponentFixture } from "@angular/core/testing" import { CommonModule } from "@angular/common" import { NehubaViewerUnit, IMPORT_NEHUBA_INJECT_TOKEN, scanFn } from "./nehubaViewer.component" -import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service" import { LoggingModule, LoggingService } from "src/logging" import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants" import { Subject } from "rxjs" @@ -106,7 +105,6 @@ describe('> nehubaViewer.component.ts', () => { provide: SET_COLORMAP_OBS, useValue: setcolorMap$ }, - AtlasWorkerService, LoggingService, ] }).compileComponents() diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts index cfd902f1a76b4dc530b4d30050c96d70e1edff05..4d38f0487b48b24a678e8a34b454d521d102e94e 100644 --- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts +++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts @@ -1,7 +1,6 @@ import { Component, ElementRef, EventEmitter, OnDestroy, Output, Inject, Optional } from "@angular/core"; -import { fromEvent, Subscription, BehaviorSubject, Observable, Subject, of, interval } from 'rxjs' -import { debounceTime, filter, map, scan, switchMap, take, distinctUntilChanged, debounce } from "rxjs/operators"; -import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service"; +import { Subscription, BehaviorSubject, Observable, Subject, of, interval } from 'rxjs' +import { debounceTime, filter, scan, switchMap, take, distinctUntilChanged, debounce } from "rxjs/operators"; import { LoggingService } from "src/logging"; import { bufferUntil, getExportNehuba, getViewer, setNehubaViewer, switchMapWaitFor } from "src/util/fn"; import { deserializeSegment, NEHUBA_INSTANCE_INJTKN } from "../util"; @@ -135,7 +134,6 @@ export class NehubaViewerUnit implements OnDestroy { constructor( public elementRef: ElementRef, - private workerService: AtlasWorkerService, private log: LoggingService, @Inject(IMPORT_NEHUBA_INJECT_TOKEN) getImportNehubaPr: () => Promise<any>, @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Subject<NehubaViewerUnit>, @@ -179,67 +177,6 @@ export class NehubaViewerUnit implements OnDestroy { }) .catch(e => this.errorEmitter.emit(e)) - - /** - * TODO move to layerCtrl.service - */ - this.ondestroySubscriptions.push( - fromEvent(this.workerService.worker, 'message').pipe( - filter((message: any) => { - - if (!message) { - // this.log.error('worker response message is undefined', message) - return false - } - if (!message.data) { - // this.log.error('worker response message.data is undefined', message.data) - return false - } - if (message.data.type !== 'ASSEMBLED_USERLANDMARKS_VTK') { - /* worker responded with not assembled landmark, no need to act */ - return false - } - /** - * nb url may be undefined - * if undefined, user have removed all user landmarks, and all that needs to be done - * is remove the user landmark layer - * - * message.data.url - */ - - return true - }), - debounceTime(100), - map(e => e.data.url), - ).subscribe(url => { - this.landmarksLoaded = !!url - this.removeuserLandmarks() - - /** - * url may be null if user removes all landmarks - */ - if (!url) { - /** - * remove transparency from meshes in current layer(s) - */ - this.setMeshTransparency(false) - return - } - const _ = {} - _[NG_USER_LANDMARK_LAYER_NAME] = { - type: 'mesh', - source: `vtk://${url}`, - shader: this.userLandmarkShader, - } - this.loadLayer(_) - - /** - * adding transparency to meshes in current layer(s) - */ - this.setMeshTransparency(true) - }), - ) - if (this.setColormap$) { this.ondestroySubscriptions.push( this.setColormap$.pipe( @@ -349,6 +286,12 @@ export class NehubaViewerUnit implements OnDestroy { this.setLayerTransparency(key, p.payload[key]) } } + if (message.type === "updateShader") { + const p = message as TNgLayerCtrl<'updateShader'> + for (const key in p.payload) { + this.setLayerShader(key, p.payload[key]) + } + } } }) ) @@ -540,37 +483,6 @@ export class NehubaViewerUnit implements OnDestroy { } private userLandmarkShader: string = FRAGMENT_MAIN_WHITE - - // TODO single landmark for user landmark - public updateUserLandmarks(landmarks: any[]) { - if (!this.nehubaViewer) { - return - } - - this.workerService.worker.postMessage({ - type : 'GET_USERLANDMARKS_VTK', - scale: Math.min(...this.dim.map(v => v * NG_LANDMARK_CONSTANT)), - landmarks : landmarks.map(lm => lm.position.map(coord => coord * 1e6)), - }) - - const parseLmColor = lm => { - if (!lm) return null - const { color } = lm - if (!color) return null - if (!Array.isArray(color)) return null - if (color.length !== 3) return null - const parseNum = num => (num >= 0 && num <= 255 ? num / 255 : 1).toFixed(3) - return `emitRGB(vec3(${color.map(parseNum).join(',')}));` - } - - const appendConditional = (frag, idx) => frag && `if (label > ${idx - 0.01} && label < ${idx + 0.01}) { ${frag} }` - - if (landmarks.some(parseLmColor)) { - this.userLandmarkShader = `void main(){ ${landmarks.map(parseLmColor).map(appendConditional).filter(v => !!v).join('else ')} else {${FRAGMENT_EMIT_WHITE}} }` - } else { - this.userLandmarkShader = FRAGMENT_MAIN_WHITE - } - } public removeSpatialSearch3DLandmarks() { this.removeLayer({ @@ -796,6 +708,11 @@ export class NehubaViewerUnit implements OnDestroy { if (layer.layer.opacity) layer.layer.opacity.restoreState(alpha) } + private setLayerShader(layerName: string, shader: string) { + const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName) + if (layer?.layer?.fragmentMain) layer.layer.fragmentMain.restoreState(shader) + } + public setMeshTransparency(flag: boolean){ /** diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index 0c7b084eb8dab41764d88d913578e8eeb843478b..5c16b0a065f1f5b2d14be81d724230ed30b7b301 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -20,6 +20,7 @@ import { SapiRegionModel } from "src/atlasComponents/sapi"; import { NehubaConfig, getParcNgId, getRegionLabelIndex } from "../config.service"; import { SET_MESHES_TO_LOAD } from "../constants"; import { annotation, atlasAppearance, atlasSelection, userInteraction } from "src/state"; +import { linearTransform, TVALID_LINEAR_XFORM_DST, TVALID_LINEAR_XFORM_SRC } from "src/atlasComponents/sapi/core/space/interspaceLinearXform"; export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!` @@ -284,8 +285,45 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnDestroy, AfterViewIni /** * TODO check extension? */ - this.dismissAllAddedLayers() + + if (/\.swc$/i.test(file.name)) { + let message = `The swc rendering is experimental. Please contact us on any feedbacks. ` + const swcText = await file.text() + let src: TVALID_LINEAR_XFORM_SRC + const dst: TVALID_LINEAR_XFORM_DST = "NEHUBA" + if (/ccf/i.test(swcText)) { + src = "CCF" + message += `CCF detected, applying known transformation.` + } + if (!src) { + message += `no known space detected. Applying default transformation.` + } + + const xform = await linearTransform(src, dst) + + const url = URL.createObjectURL(file) + this.droppedLayerNames.push({ + layerName: randomUuid, + resourceUrl: url + }) + this.store$.dispatch( + atlasAppearance.actions.addCustomLayer({ + customLayer: { + id: randomUuid, + source: `swc://${url}`, + segments: ["1"], + transform: xform, + clType: 'customlayer/nglayer' as const + } + }) + ) + this.snackbar.open(message, "Dismiss", { + duration: 10000 + }) + return + } + // Get file, try to inflate, if files, use original array buffer const buf = await file.arrayBuffer() diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts index fd216151b9bbbf14d3b498eae8ffc98b2501ba05..ea321e012cccbbe69e27f0e8467b14b945876e00 100644 --- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts +++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.component.ts @@ -1,44 +1,22 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges, OnDestroy } from "@angular/core"; import { Store } from "@ngrx/store"; import { isMat4 } from "common/util" +import { CONST } from "common/constants" import { Observable } from "rxjs"; -import { atlasAppearance } from "src/state"; +import { atlasAppearance, atlasSelection } from "src/state"; import { NehubaViewerUnit } from ".."; import { NEHUBA_INSTANCE_INJTKN } from "../util"; +import { getExportNehuba } from "src/util/fn"; type Vec4 = [number, number, number, number] type Mat4 = [Vec4, Vec4, Vec4, Vec4] -const _VOL_DETAIL_MAP: Record<string, { shader: string, opacity: number }> = { - "PLI Fiber Orientation Red Channel": { - shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(1.0 * x, x * 0., 0. * x )); } }", - opacity: 1 - }, - "PLI Fiber Orientation Green Channel": { - shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(0. * x, x * 1., 0. * x )); } }", - opacity: 0.5 - }, - "PLI Fiber Orientation Blue Channel": { - shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(0. * x, x * 0., 1.0 * x )); } }", - opacity: 0.25 - }, - "Blockface Image": { - shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(0.8 * x, x * 1., 0.8 * x )); } }", - opacity: 1.0 - }, - "PLI Transmittance": { - shader: "void main(){ float x = toNormalized(getDataValue()); if (x > 0.9) { emitTransparent(); } else { emitRGB(vec3(x * 1., x * 0.8, x * 0.8 )); } }", - opacity: 1.0 - }, - "T2w MRI": { - shader: "void main(){ float x = toNormalized(getDataValue()); if (x < 0.1) { emitTransparent(); } else { emitRGB(vec3(0.8 * x, 0.8 * x, x * 1. )); } }", - opacity: 1 - }, - "MRI Labels": { - shader: null, - opacity: 1 - } -} +export const idMat4: Mat4 = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], +] @Component({ selector: 'ng-layer-ctl', @@ -51,9 +29,15 @@ const _VOL_DETAIL_MAP: Record<string, { shader: string, opacity: number }> = { export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ + public CONST = CONST + private onDestroyCb: (() => void)[] = [] private removeLayer: () => void + public showOpacityCtrl = false + public hideNgTuneCtrl = 'lower_threshold,higher_threshold,brightness,contrast,colormap,hide-threshold-checkbox' + public defaultOpacity = 1 + @Input('ng-layer-ctl-name') name: string @@ -73,7 +57,8 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ this.opacity = Number(val) } - transform: Mat4 + transform: Mat4 = idMat4 + @Input('ng-layer-ctl-transform') set _transform(xform: string | Mat4) { const parsedResult = typeof xform === "string" @@ -107,12 +92,6 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ } ngOnChanges(): void { - if (this.name in _VOL_DETAIL_MAP) { - const { shader, opacity } = _VOL_DETAIL_MAP[this.name] - this.shader = shader - this.opacity = opacity - } - if (this.name && this.source) { const { name } = this if (this.removeLayer) { @@ -141,6 +120,28 @@ export class NgLayerCtrlCmp implements OnChanges, OnDestroy{ } } + setOrientation(): void { + const { mat4, quat, vec3 } = getExportNehuba() + + /** + * glMatrix seems to store the matrix in transposed format + */ + + const incM = mat4.transpose(mat4.create(), mat4.fromValues(...this.transform.reduce((acc, curr) => [...acc, ...curr], []))) + const scale = mat4.getScaling(vec3.create(), incM) + const scaledM = mat4.scale(mat4.create(), incM, vec3.inverse(vec3.create(), scale)) + const q = mat4.getRotation(quat.create(0), scaledM) + + this.store.dispatch( + atlasSelection.actions.navigateTo({ + navigation: { + orientation: Array.from(q) + }, + animation: true + }) + ) + } + toggleVisibility(): void{ this.visible = !this.visible this.viewer.nehubaViewer.ngviewer.layerManager.getLayerByName(this.name).setVisible(this.visible) diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..b716f7d93824fd1e223450fde56655398aa91d0b --- /dev/null +++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.stories.ts @@ -0,0 +1,60 @@ +import { idMat4, NgLayerCtrlCmp } from "./ngLayerCtrl.component" +import { Meta, moduleMetadata, Story } from "@storybook/angular" +import { CommonModule } from "@angular/common" +import { MatButtonModule } from "@angular/material/button" +import { NEHUBA_INSTANCE_INJTKN } from "../util" +import { NEVER } from "rxjs" +import { action } from "@storybook/addon-actions" +import { MatTooltipModule } from "@angular/material/tooltip" +import { Store } from "@ngrx/store" + +export default { + component: NgLayerCtrlCmp, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + MatButtonModule, + MatTooltipModule, + ], + providers: [ + { + provide: NEHUBA_INSTANCE_INJTKN, + useValue: NEVER, + }, + { + provide: Store, + useValue: { + dispatch: action('dispatch') + } + } + ] + }), + ] +} as Meta + + +const Template: Story<NgLayerCtrlCmp> = (args: any, { parameters }) => { + + const { + 'ng-layer-ctl-name': name, + 'ng-layer-ctl-transform': transform + } = args + + const { + pName, + pXform + } = parameters + + return { + props: { + name: name || pName || 'default name', + transform: transform || pXform || idMat4, + } + } +} + +export const NgLayerTune = Template.bind({}) +NgLayerTune.parameters = { + pXform: [[-0.74000001,0,0,38134608],[0,-0.26530117,-0.6908077,13562314],[0,-0.6908077,0.26530117,-3964904],[0,0,0,1]] +} diff --git a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html index 2188b17d9f57f496928a0b40f2ba78e3508be9d5..b53ec540ad7fd2bcf7df004327d9cc3f2b3ba51b 100644 --- a/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html +++ b/src/viewerModule/nehuba/ngLayerCtl/ngLayerCtrl.template.html @@ -1,10 +1,34 @@ <div [ngClass]="{ 'text-muted': !visible }"> - <button mat-icon-button (click)="toggleVisibility()"> + <button mat-icon-button + [matTooltip]="CONST.TOGGLE_LAYER_VISILITY" + (click)="toggleVisibility()"> <i [ngClass]="visible ? 'fa-eye' : 'fa-eye-slash'" class="far"></i> </button> <span> {{ name }} </span> + + <button + mat-icon-button + [matTooltip]="CONST.ORIENT_TO_LAYER" + (click)="setOrientation()"> + <i class="iavic iavic-rotation"></i> + </button> + + <button + mat-icon-button + [matTooltip]="CONST.CONFIGURE_LAYER" + (click)="showOpacityCtrl = !showOpacityCtrl"> + <i class="fas fa-cog"></i> + </button> + + <ng-template [ngIf]="showOpacityCtrl"> + <ng-layer-tune + [ngLayerName]="name" + [hideCtrl]="hideNgTuneCtrl" + [opacity]="defaultOpacity"> + </ng-layer-tune> + </ng-template> </div> diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts index ce0afa8fa8632b433b5998eed1107654fdc4dfa2..ab6ac511153553a1bc50365bd2b85c4a239c94f4 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.spec.ts @@ -146,8 +146,8 @@ describe('> statusCard.component.ts', () => { initialNgState: { navigation: { pose: { - orientation: [0,0,0,1], - position: [10, 20, 30] + orientation: [0, 0, 0, 1], + position: [0, 0, 0] }, zoomFactor: 1e6 } diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts index 26d6786e8b7f7ea3723eb47d7a32bc299c46b720..58bbdf911ed843cd5e68123f65869fe5843b3ace 100644 --- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts +++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts @@ -177,10 +177,7 @@ export class StatusCardComponent implements OnInit, OnChanges{ */ public resetNavigation({rotation: rotationFlag = false, position: positionFlag = false, zoom : zoomFlag = false}: {rotation?: boolean, position?: boolean, zoom?: boolean}): void { const config = getNehubaConfig(this.selectedTemplate) - const { - orientation, - position - } = config.dataset.initialNgState.navigation.pose + const { zoomFactor: zoom } = config.dataset.initialNgState.navigation @@ -189,8 +186,8 @@ export class StatusCardComponent implements OnInit, OnChanges{ actions.navigateTo({ navigation: { ...this.currentNavigation, - ...(rotationFlag ? { orientation: orientation } : {}), - ...(positionFlag ? { position: position } : {}), + ...(rotationFlag ? { orientation: [0, 0, 0, 1] } : {}), + ...(positionFlag ? { position: [0, 0, 0] } : {}), ...(zoomFlag ? { zoom: zoom } : {}), }, physical: true, diff --git a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts index c5042ecfc22e3134ab6af2c967bdad86bf5d4933..0ec1d5125ba83f89dc91ea8e20e33954673030a7 100644 --- a/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts +++ b/src/viewerModule/nehuba/viewerCtrl/viewerCtrlCmp/viewerCtrlCmp.component.spec.ts @@ -22,7 +22,6 @@ describe('> viewerCtrlCmp.component.ts', () => { let mockStore: MockStore let mockNehubaViewer = { - updateUserLandmarks: jasmine.createSpy(), nehubaViewer: { ngviewer: { layerManager: { @@ -42,7 +41,6 @@ describe('> viewerCtrlCmp.component.ts', () => { } afterEach(() => { - mockNehubaViewer.updateUserLandmarks.calls.reset() mockNehubaViewer.nehubaViewer.ngviewer.layerManager.getLayerByName.calls.reset() mockNehubaViewer.nehubaViewer.ngviewer.display.scheduleRedraw.calls.reset() }) diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts index a194176aa6d83027be793d3ebe1d098aa75f8c8d..f04ad6cc1b0a5b85875c6bc247150b737eb5605c 100644 --- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts +++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts @@ -328,7 +328,8 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, AfterViewInit * subscribe to main store and negotiate with relay to set camera */ const navSub = this.store$.pipe( - select(atlasSelection.selectors.navigation) + select(atlasSelection.selectors.navigation), + filter(v => !!v), ).subscribe(nav => { const { perspectiveOrientation, perspectiveZoom } = nav this.mainStoreCameraNav = { diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index d28f45292763e4b62385e8829198849d62b8af58..f0b32efea3c8233a648e0277bfd38ddeaebe2074 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -12,6 +12,9 @@ import { SAPI, SapiRegionModel } from "src/atlasComponents/sapi"; import { atlasSelection, userInteraction, } from "src/state"; import { SapiSpatialFeatureModel, SapiFeatureModel, SapiParcellationModel, SapiSpaceModel } from "src/atlasComponents/sapi/type"; import { getUuid } from "src/util/fn"; +import { environment } from "src/environments/environment" +import { SapiViewsFeaturesVoiQuery } from "src/atlasComponents/sapiViews/features"; +import { SapiViewsCoreSpaceBoundingBox } from "src/atlasComponents/sapiViews/core"; @Component({ selector: 'iav-cmp-viewer-container', @@ -62,10 +65,17 @@ export class ViewerCmp implements OnDestroy { public CONST = CONST public ARIA_LABELS = ARIA_LABELS + public VOI_QUERY_FLAG = environment.EXPERIMENTAL_FEATURE_FLAG @ViewChild('genericInfoVCR', { read: ViewContainerRef }) genericInfoVCR: ViewContainerRef + @ViewChild('voiFeatures', { read: SapiViewsFeaturesVoiQuery }) + voiQueryDirective: SapiViewsFeaturesVoiQuery + + @ViewChild('bbox', { read: SapiViewsCoreSpaceBoundingBox }) + boundingBoxDirective: SapiViewsCoreSpaceBoundingBox + public quickTourRegionSearch: IQuickTourData = { order: 7, description: QUICKTOUR_DESC.REGION_SEARCH, @@ -321,6 +331,7 @@ export class ViewerCmp implements OnDestroy { switch(event.type) { case EnumViewerEvt.VIEWERLOADED: this.viewerLoaded = event.data + this.cdr.detectChanges() break case EnumViewerEvt.VIEWER_CTX: this.ctxMenuSvc.context$.next(event.data) diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css index 671fef08c4faf6d72e1cfd3c9664208fb60261b7..e5a409a28036bfe88dab0ce9457272ed7647cf60 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.style.css +++ b/src/viewerModule/viewerCmp/viewerCmp.style.css @@ -118,3 +118,25 @@ mat-list[dense].contextual-block { background-color : rgba(30,30,30,0.8); } + +.region-populated +{ + overflow: hidden auto; +} + +.region-chip-suffix +{ + transform: scale(0.7); + margin-right: -0.25rem; +} + +.leap-control-wrapper +{ + width: 0; + height: 0; + overflow: visible; + flex-direction: row; + display: flex; + align-items: flex-end; + justify-content: flex-end; +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 891cdb9f42e5cecfe7732c6004bdaae24a2dab6a..b0e06fdaab9dcdca386cf69563c8334f2313336e 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -4,12 +4,12 @@ <div class="floating-ui"> - <div *ngIf="(media.mediaBreakPoint$ | async) < 3" - class="fixed-bottom pe-none mb-2 d-flex justify-content-center"> + <div *ngIf="(media.mediaBreakPoint$ | async) < 2" + class="fixed-bottom sxplr-pe-none mb-2 d-flex justify-content-center"> <logo-container></logo-container> </div> - <div *ngIf="(media.mediaBreakPoint$ | async) < 3" floatingMouseContextualContainerDirective> + <div *ngIf="(media.mediaBreakPoint$ | async) < 2" floatingMouseContextualContainerDirective> <div class="h-0" iav-mouse-hover @@ -29,7 +29,7 @@ </mat-list-item> - <ng-template [ngIf]="voiFeatures.onhover | async" let-feat> + <ng-template [ngIf]="voiQueryDirective && (voiQueryDirective.onhover | async)" let-feat> <mat-list-item> <mat-icon fontSet="fas" @@ -99,7 +99,7 @@ </mat-drawer> <!-- master content --> - <mat-drawer-content class="visible pe-none position-relative"> + <mat-drawer-content class="visible sxplr-pe-none position-relative"> <iav-layout-fourcorners> <!-- top left --> @@ -148,7 +148,7 @@ <!-- bottom left --> - <div iavLayoutFourCornersBottomLeft class="ws-no-wrap d-inline-flex w-100vw pe-none align-items-center mb-4"> + <div iavLayoutFourCornersBottomLeft class="ws-no-wrap d-inline-flex w-100vw sxplr-pe-none align-items-center mb-4"> <!-- special bottom left --> <ng-template [ngIf]="viewerMode$ | async" let-mode [ngIfElse]="localBottomLeftTmpl"></ng-template> @@ -169,6 +169,13 @@ </ng-template> </div> + + <!-- buttom right --> + <div iavLayoutFourCornersBottomRight> + <div class="leap-control-wrapper"> + <div leap-control-view-ref></div> + </div> + </div> </iav-layout-fourcorners> </mat-drawer-content> </mat-drawer-container> @@ -223,9 +230,11 @@ <ng-container *ngTemplateOutlet="autocompleteTmpl; context: { showTour: true }"> </ng-container> - <div *ngIf="!((selectedRegions$ | async)[0])" class="sxplr-p-2 w-100"> - <ng-container *ngTemplateOutlet="spatialFeatureListViewTmpl"></ng-container> - </div> + <ng-template [ngIf]="VOI_QUERY_FLAG"> + <div *ngIf="!((selectedRegions$ | async)[0])" class="sxplr-p-2 w-100"> + <ng-container *ngTemplateOutlet="spatialFeatureListViewTmpl"></ng-container> + </div> + </ng-template> </div> <!-- such a gross implementation --> @@ -264,7 +273,7 @@ isOpen: minTrayVisSwitch.switchState$ | async, regionSelected: selectedRegions$ | async, click: minTrayVisSwitch.toggle.bind(minTrayVisSwitch), - badge: (voiFeatures.features$ | async).length || null + badge: voiQueryDirective && (voiQueryDirective.features$ | async).length || null }"> </ng-container> </div> @@ -434,7 +443,7 @@ <div prefix> </div> - <div suffix class="sxplr-scale-70"> + <div suffix class="region-chip-suffix"> <button mat-mini-fab color="primary" iav-stop="mousedown click" @@ -543,7 +552,7 @@ <i class="fas fa-sitemap"></i> </button> - <div class="w-100 h-100 position-absolute pe-none" *ngIf="showTour"> + <div class="w-100 h-100 position-absolute sxplr-pe-none" *ngIf="showTour"> </div> </div> @@ -683,7 +692,7 @@ <!-- see https://github.com/HumanBrainProject/interactive-viewer/issues/698 --> - <ng-template [ngIf]="regionDirective.fetchInProgress"> + <ng-template [ngIf]="regionDirective.fetchInProgress$ | async"> <spinner-cmp class="sxplr-mt-10 fs-200"></spinner-cmp> </ng-template> <sxplr-sapiviews-core-region-region-rich @@ -983,18 +992,18 @@ </ng-template> <ng-template #spatialFeatureListViewTmpl> - <div *ngIf="voiFeatures.busy$ | async; else notBusyTmpl" class="fs-200"> + <div *ngIf="voiQueryDirective && (voiQueryDirective.busy$ | async); else notBusyTmpl" class="fs-200"> <spinner-cmp></spinner-cmp> </div> <ng-template #notBusyTmpl> - <mat-card *ngIf="(voiFeatures.features$ | async).length > 0" class="pe-all mat-elevation-z4"> + <mat-card *ngIf="voiQueryDirective && (voiQueryDirective.features$ | async).length > 0" class="pe-all mat-elevation-z4"> <mat-card-title> Volumes of interest </mat-card-title> <mat-card-subtitle class="overflow-hidden"> <!-- TODO in future, perhaps encapsulate this as a component? seems like a nature fit in sapiView/space/boundingbox --> - <ng-template let-bbox [ngIf]="bbox.bbox$ | async | getProperty : 'bbox'" [ngIfElse]="bboxFallbackTmpl"> + <ng-template let-bbox [ngIf]="boundingBoxDirective && (boundingBoxDirective.bbox$ | async | getProperty : 'bbox')" [ngIfElse]="bboxFallbackTmpl"> Bounding box: {{ bbox[0] | numbers | json }} - {{ bbox[1] | numbers | json }} mm </ng-template> <ng-template #bboxFallbackTmpl> @@ -1005,17 +1014,21 @@ <mat-divider></mat-divider> - <div *ngFor="let feature of voiFeatures.features$ | async" - mat-ripple - (click)="showDataset(feature)" - class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses"> - {{ feature.metadata.fullName }} - </div> + <ng-template [ngIf]="voiQueryDirective"> + + <div *ngFor="let feature of voiQueryDirective.features$ | async" + mat-ripple + (click)="showDataset(feature)" + class="sxplr-custom-cmp hoverable w-100 overflow-hidden text-overflow-ellipses"> + {{ feature.metadata.fullName }} + </div> + </ng-template> </mat-card> </ng-template> </ng-template> <div class="d-none" + *ngIf="VOI_QUERY_FLAG" sxplr-sapiviews-core-space-boundingbox [sxplr-sapiviews-core-space-boundingbox-atlas]="selectedAtlas$ | async" [sxplr-sapiviews-core-space-boundingbox-space]="templateSelected$ | async" diff --git a/src/widget/constants.ts b/src/widget/constants.ts index 412bccd405d746982f24081f8d405cd7011aeb53..53cc6568a34d662fa229e00592ca9b92ba0a3e08 100644 --- a/src/widget/constants.ts +++ b/src/widget/constants.ts @@ -21,3 +21,11 @@ interface TypeActionWidgetReturnVal<T>{ export type TypeActionToWidget<T> = (type: EnumActionToWidget, obj: T, option: IActionWidgetOption) => TypeActionWidgetReturnVal<T> export const WIDGET_PORTAL_TOKEN = new InjectionToken<Record<string, unknown>>("WIDGET_PORTAL_TOKEN") + +export const RM_WIDGET = new InjectionToken('RM_WIDGET') + +export enum EnumWidgetState { + MINIMIZED, + NORMAL, + MAXIMIZED, +} diff --git a/src/widget/service.ts b/src/widget/service.ts index 75be508867a61aa48951701ff91a43dbab8045cd..9430269418731ceb3b57810b54b26ef155ef7ef0 100644 --- a/src/widget/service.ts +++ b/src/widget/service.ts @@ -1,5 +1,6 @@ import { ComponentPortal } from "@angular/cdk/portal"; import { ComponentFactory, ComponentFactoryResolver, ComponentRef, Injectable, Injector, ViewContainerRef } from "@angular/core"; +import { RM_WIDGET } from "./constants"; import { WidgetPortal } from "./widgetPortal/widgetPortal.component"; @Injectable({ @@ -18,8 +19,15 @@ export class WidgetService { } public addNewWidget<T>(Component: new (...arg: any) => T, injector: Injector): WidgetPortal<T> { - const widgetPortal = this.vcr.createComponent(this.cf, 0, injector) as ComponentRef<WidgetPortal<T>> - const cmpPortal = new ComponentPortal<T>(Component, this.vcr, injector) + const inj = Injector.create({ + providers: [{ + provide: RM_WIDGET, + useValue: (cmp: WidgetPortal<T>) => this.rmWidget(cmp) + }], + parent: injector + }) + const widgetPortal = this.vcr.createComponent(this.cf, 0, inj) as ComponentRef<WidgetPortal<T>> + const cmpPortal = new ComponentPortal<T>(Component, this.vcr, inj) this.viewRefMap.set(widgetPortal.instance, widgetPortal) diff --git a/src/widget/widget.module.ts b/src/widget/widget.module.ts index 138ed66482a78ad917959b416aca333c415c09b0..62b9d65ba70505d09aef098617ec2681bb657294 100644 --- a/src/widget/widget.module.ts +++ b/src/widget/widget.module.ts @@ -2,17 +2,20 @@ import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { ComponentsModule } from "src/components"; import { WidgetCanvas } from "./widgetCanvas.directive"; -import { WidgetPortal } from "./widgetPortal/widgetPortal.component"; +import { WidgetPortal } from "./widgetPortal/widgetPortal.component" import { MatCardModule } from "@angular/material/card"; import { DragDropModule } from "@angular/cdk/drag-drop"; import { MatButtonModule } from "@angular/material/button"; import { PortalModule } from "@angular/cdk/portal"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { WidgetStateIconPipe } from "./widgetStateIcon.pipe"; @NgModule({ imports:[ MatCardModule, DragDropModule, MatButtonModule, + MatTooltipModule, PortalModule, CommonModule, ComponentsModule, @@ -20,6 +23,7 @@ import { PortalModule } from "@angular/cdk/portal"; declarations: [ WidgetCanvas, WidgetPortal, + WidgetStateIconPipe, ], providers: [], exports: [ diff --git a/src/widget/widgetPortal/widgetPortal.component.ts b/src/widget/widgetPortal/widgetPortal.component.ts index a1351e15e370318c5e9bf363c4c36f2f0b8d37e9..cf7f0ea9a67e49c27812e85bef586397e84e2905 100644 --- a/src/widget/widgetPortal/widgetPortal.component.ts +++ b/src/widget/widgetPortal/widgetPortal.component.ts @@ -1,6 +1,26 @@ import { ComponentPortal } from "@angular/cdk/portal"; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from "@angular/core"; -import { WidgetService } from "../service"; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Inject, Optional } from "@angular/core"; +import { RM_WIDGET, EnumWidgetState } from "../constants"; + +type TWidgetCss = { + transform: string + width: string +} + +const widgetStateToTransform: Record<EnumWidgetState, TWidgetCss> = { + [EnumWidgetState.MINIMIZED]: { + transform: `translate(100vw, 5vh)`, + width: '24rem' + }, + [EnumWidgetState.NORMAL]: { + transform: `translate(calc(100vw - 24rem), 5vh)`, + width: '24rem' + }, + [EnumWidgetState.MAXIMIZED]: { + transform: `translate(5vw, 5vh)`, + width: '90vw' + }, +} @Component({ selector: 'sxplr-widget-portal', @@ -13,6 +33,8 @@ import { WidgetService } from "../service"; export class WidgetPortal<T>{ + EnumWidgetState = EnumWidgetState + portal: ComponentPortal<T> private _name: string @@ -24,18 +46,38 @@ export class WidgetPortal<T>{ this.cdr.markForCheck() } - defaultPosition = { - x: 200, - y: 200, + minimizeReturnState: EnumWidgetState.NORMAL | EnumWidgetState.MAXIMIZED + + private _state: EnumWidgetState = EnumWidgetState.NORMAL + get state() { + return this._state } + set state(val: EnumWidgetState) { + if (val === EnumWidgetState.MINIMIZED) { + this.minimizeReturnState = this._state !== EnumWidgetState.MINIMIZED + ? this._state + : EnumWidgetState.NORMAL + } + this._state = val + this.transform = widgetStateToTransform[this._state]?.transform || widgetStateToTransform[EnumWidgetState.NORMAL].transform + this.width = widgetStateToTransform[this._state]?.width || widgetStateToTransform[EnumWidgetState.NORMAL].width + + this.cdr.markForCheck() + } + + @HostBinding('style.transform') + transform = widgetStateToTransform[ EnumWidgetState.NORMAL ].transform + + @HostBinding('style.width') + width = widgetStateToTransform[ EnumWidgetState.NORMAL ].width constructor( - private wSvc: WidgetService, private cdr: ChangeDetectorRef, + @Optional() @Inject(RM_WIDGET) private rmWidget: (inst: unknown) => void ){ } exit(){ - this.wSvc.rmWidget(this) + if (this.rmWidget) this.rmWidget(this) } } diff --git a/src/widget/widgetPortal/widgetPortal.style.css b/src/widget/widgetPortal/widgetPortal.style.css index 12e9c809623d27f95eef3986c869ed9966496328..3b374eee2ca51e3338e64c35f48593e33f66623f 100644 --- a/src/widget/widgetPortal/widgetPortal.style.css +++ b/src/widget/widgetPortal/widgetPortal.style.css @@ -2,15 +2,18 @@ { pointer-events: none; display: block; - max-width: 24rem; + + width: 24rem; + height: 90vh; + + transition: all 160ms cubic-bezier(0.35, 0, 0.25, 1); } mat-card { pointer-events: all; - max-width: 36vw; - height: 36rem; - max-height: 90vh; + width: 100%; + height: 100%; } mat-card-content @@ -24,7 +27,7 @@ mat-card-content .widget-portal-header { display: flex; - justify-content: space-between; + justify-content: flex-end; align-items: center; } @@ -33,24 +36,13 @@ mat-card-content flex-grow: 1; } -.hover-grab -{ - opacity: 0.5; - transition: opacity 200ms ease-in-out; - cursor: move; -} - -.hover-grab:hover -{ - opacity: 1.0; -} - -.widget-grab-handle -{ - margin-right:1rem; -} - .widget-name { flex-grow: 1; } + +.when-minimized-nub +{ + position: absolute; + transform: translate(-5rem, 5rem); +} \ No newline at end of file diff --git a/src/widget/widgetPortal/widgetPortal.template.html b/src/widget/widgetPortal/widgetPortal.template.html index 7aff890d9ecab44127f5b70a0df3a5ba0f5c305c..5e4e495fa8894b2b9f8e81e0ddb67b5120f4c081 100644 --- a/src/widget/widgetPortal/widgetPortal.template.html +++ b/src/widget/widgetPortal/widgetPortal.template.html @@ -1,13 +1,41 @@ -<mat-card cdkDrag [cdkDragFreeDragPosition]="defaultPosition"> +<div *ngIf="state === EnumWidgetState.MINIMIZED" + class="when-minimized-nub"> + + <button mat-mini-fab + [matTooltip]="name" + color="primary" + class="sxplr-pe-all" + (click)="state = minimizeReturnState"> + <i [class]="minimizeReturnState | widgetStateIcon"></i> + </button> +</div> + +<mat-card> <mat-card-content> - <div class="widget-portal-header" cdkDragHandle> - <span class="hover-grab widget-grab-handle"> - <i class="fas fa-grip-vertical"></i> - </span> + <div class="widget-portal-header"> <span *ngIf="name" class="widget-name"> {{ name }} </span> + + <!-- state changer --> + <ng-template [ngTemplateOutlet]="stateBtnTmpl" + [ngTemplateOutletContext]="{ + $implicit: EnumWidgetState.MINIMIZED + }"> + </ng-template> + + <ng-template [ngTemplateOutlet]="stateBtnTmpl" + [ngTemplateOutletContext]="{ + $implicit: EnumWidgetState.NORMAL + }"> + </ng-template> + + <ng-template [ngTemplateOutlet]="stateBtnTmpl" + [ngTemplateOutletContext]="{ + $implicit: EnumWidgetState.MAXIMIZED + }"> + </ng-template> <button mat-icon-button (click)="exit()"> <i class="fas fa-times"></i> @@ -20,3 +48,13 @@ </div> </mat-card-content> </mat-card> + +<!-- template for plugin state --> +<ng-template #stateBtnTmpl let-btnstate> + <button + *ngIf="state !== btnstate" + (click)="state = btnstate" + mat-icon-button> + <i [class]="btnstate | widgetStateIcon"></i> + </button> +</ng-template> diff --git a/src/widget/widgetStateIcon.pipe.ts b/src/widget/widgetStateIcon.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7af88175fe3d66886e68610a296db89c7e79fc6 --- /dev/null +++ b/src/widget/widgetStateIcon.pipe.ts @@ -0,0 +1,26 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { EnumWidgetState } from "./constants" + +@Pipe({ + name: 'widgetStateIcon', + pure: true +}) + +export class WidgetStateIconPipe implements PipeTransform{ + public transform(state: EnumWidgetState): string { + switch (state) { + case EnumWidgetState.MINIMIZED: { + return 'fas fa-window-minimize' + } + case EnumWidgetState.NORMAL: { + return 'fas fa-window-restore' + } + case EnumWidgetState.MAXIMIZED: { + return 'fas fa-window-maximize' + } + default: { + return 'fas fa-window-restore' + } + } + } +} diff --git a/third_party/leap-0.6.4.js b/third_party/leap-0.6.4.js new file mode 100644 index 0000000000000000000000000000000000000000..575679f9535222b58114e1617cdd714953e2230a --- /dev/null +++ b/third_party/leap-0.6.4.js @@ -0,0 +1,9420 @@ +/*! + * LeapJS v0.6.4 + * http://github.com/leapmotion/leapjs/ + * + * Copyright 2013 LeapMotion, Inc. and other contributors + * Released under the Apache-2.0 license + * http://github.com/leapmotion/leapjs/blob/master/LICENSE.txt + */ +;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){ +var Pointable = require('./pointable'), + glMatrix = require("gl-matrix") + , vec3 = glMatrix.vec3 + , mat3 = glMatrix.mat3 + , mat4 = glMatrix.mat4 + , _ = require('underscore'); + + +var Bone = module.exports = function(finger, data) { + this.finger = finger; + + this._center = null, this._matrix = null; + + /** + * An integer code for the name of this bone. + * + * * 0 -- metacarpal + * * 1 -- proximal + * * 2 -- medial + * * 3 -- distal + * * 4 -- arm + * + * @member type + * @type {number} + * @memberof Leap.Bone.prototype + */ + this.type = data.type; + + /** + * The position of the previous, or base joint of the bone closer to the wrist. + * @type {vector3} + */ + this.prevJoint = data.prevJoint; + + /** + * The position of the next joint, or the end of the bone closer to the finger tip. + * @type {vector3} + */ + this.nextJoint = data.nextJoint; + + /** + * The estimated width of the tool in millimeters. + * + * The reported width is the average width of the visible portion of the + * tool from the hand to the tip. If the width isn't known, + * then a value of 0 is returned. + * + * Pointable objects representing fingers do not have a width property. + * + * @member width + * @type {number} + * @memberof Leap.Pointable.prototype + */ + this.width = data.width; + + var displacement = new Array(3); + vec3.sub(displacement, data.nextJoint, data.prevJoint); + + this.length = vec3.length(displacement); + + + /** + * + * These fully-specify the orientation of the bone. + * See examples/threejs-bones.html for more info + * Three vec3s: + * x (red): The rotation axis of the finger, pointing outwards. (In general, away from the thumb ) + * y (green): The "up" vector, orienting the top of the finger + * z (blue): The roll axis of the bone. + * + * Most up vectors will be pointing the same direction, except for the thumb, which is more rightwards. + * + * The thumb has one fewer bones than the fingers, but there are the same number of joints & joint-bases provided + * the first two appear in the same position, but only the second (proximal) rotates. + * + * Normalized. + */ + this.basis = data.basis; +}; + +Bone.prototype.left = function(){ + + if (this._left) return this._left; + + this._left = mat3.determinant(this.basis[0].concat(this.basis[1]).concat(this.basis[2])) < 0; + + return this._left; + +}; + + +/** + * The Affine transformation matrix describing the orientation of the bone, in global Leap-space. + * It contains a 3x3 rotation matrix (in the "top left"), and center coordinates in the fourth column. + * + * Unlike the basis, the right and left hands have the same coordinate system. + * + */ +Bone.prototype.matrix = function(){ + + if (this._matrix) return this._matrix; + + var b = this.basis, + t = this._matrix = mat4.create(); + + // open transform mat4 from rotation mat3 + t[0] = b[0][0], t[1] = b[0][1], t[2] = b[0][2]; + t[4] = b[1][0], t[5] = b[1][1], t[6] = b[1][2]; + t[8] = b[2][0], t[9] = b[2][1], t[10] = b[2][2]; + + t[3] = this.center()[0]; + t[7] = this.center()[1]; + t[11] = this.center()[2]; + + if ( this.left() ) { + // flip the basis to be right-handed + t[0] *= -1; + t[1] *= -1; + t[2] *= -1; + } + + return this._matrix; +}; + +/** + * Helper method to linearly interpolate between the two ends of the bone. + * + * when t = 0, the position of prevJoint will be returned + * when t = 1, the position of nextJoint will be returned + */ +Bone.prototype.lerp = function(out, t){ + + vec3.lerp(out, this.prevJoint, this.nextJoint, t); + +}; + +/** + * + * The center position of the bone + * Returns a vec3 array. + * + */ +Bone.prototype.center = function(){ + + if (this._center) return this._center; + + var center = vec3.create(); + this.lerp(center, 0.5); + this._center = center; + return center; + +}; + +// The negative of the z-basis +Bone.prototype.direction = function(){ + + return [ + this.basis[2][0] * -1, + this.basis[2][1] * -1, + this.basis[2][2] * -1 + ]; + +}; + +},{"./pointable":14,"gl-matrix":23,"underscore":24}],2:[function(require,module,exports){ +var CircularBuffer = module.exports = function(size) { + this.pos = 0; + this._buf = []; + this.size = size; +} + +CircularBuffer.prototype.get = function(i) { + if (i == undefined) i = 0; + if (i >= this.size) return undefined; + if (i >= this._buf.length) return undefined; + return this._buf[(this.pos - i - 1) % this.size]; +} + +CircularBuffer.prototype.push = function(o) { + this._buf[this.pos % this.size] = o; + return this.pos++; +} + +},{}],3:[function(require,module,exports){ +var chooseProtocol = require('../protocol').chooseProtocol + , EventEmitter = require('events').EventEmitter + , _ = require('underscore'); + +var BaseConnection = module.exports = function(opts) { + this.opts = _.defaults(opts || {}, { + host : '127.0.0.1', + enableGestures: false, + scheme: this.getScheme(), + port: this.getPort(), + background: false, + optimizeHMD: false, + requestProtocolVersion: BaseConnection.defaultProtocolVersion + }); + this.host = this.opts.host; + this.port = this.opts.port; + this.scheme = this.opts.scheme; + this.protocolVersionVerified = false; + this.background = null; + this.optimizeHMD = null; + this.on('ready', function() { + this.enableGestures(this.opts.enableGestures); + this.setBackground(this.opts.background); + this.setOptimizeHMD(this.opts.optimizeHMD); + + if (this.opts.optimizeHMD){ + console.log("Optimized for head mounted display usage."); + }else { + console.log("Optimized for desktop usage."); + } + + }); +}; + +// The latest available: +BaseConnection.defaultProtocolVersion = 6; + +BaseConnection.prototype.getUrl = function() { + return this.scheme + "//" + this.host + ":" + this.port + "/v" + this.opts.requestProtocolVersion + ".json"; +} + + +BaseConnection.prototype.getScheme = function(){ + return 'ws:' +} + +BaseConnection.prototype.getPort = function(){ + return 6437 +} + + +BaseConnection.prototype.setBackground = function(state) { + this.opts.background = state; + if (this.protocol && this.protocol.sendBackground && this.background !== this.opts.background) { + this.background = this.opts.background; + this.protocol.sendBackground(this, this.opts.background); + } +} + +BaseConnection.prototype.setOptimizeHMD = function(state) { + this.opts.optimizeHMD = state; + if (this.protocol && this.protocol.sendOptimizeHMD && this.optimizeHMD !== this.opts.optimizeHMD) { + this.optimizeHMD = this.opts.optimizeHMD; + this.protocol.sendOptimizeHMD(this, this.opts.optimizeHMD); + } +} + +BaseConnection.prototype.handleOpen = function() { + if (!this.connected) { + this.connected = true; + this.emit('connect'); + } +} + +BaseConnection.prototype.enableGestures = function(enabled) { + this.gesturesEnabled = enabled ? true : false; + this.send(this.protocol.encode({"enableGestures": this.gesturesEnabled})); +} + +BaseConnection.prototype.handleClose = function(code, reason) { + if (!this.connected) return; + this.disconnect(); + + // 1001 - an active connection is closed + // 1006 - cannot connect + if (code === 1001 && this.opts.requestProtocolVersion > 1) { + if (this.protocolVersionVerified) { + this.protocolVersionVerified = false; + }else{ + this.opts.requestProtocolVersion--; + } + } + this.startReconnection(); +} + +BaseConnection.prototype.startReconnection = function() { + var connection = this; + if(!this.reconnectionTimer){ + (this.reconnectionTimer = setInterval(function() { connection.reconnect() }, 500)); + } +} + +BaseConnection.prototype.stopReconnection = function() { + this.reconnectionTimer = clearInterval(this.reconnectionTimer); +} + +// By default, disconnect will prevent auto-reconnection. +// Pass in true to allow the reconnection loop not be interrupted continue +BaseConnection.prototype.disconnect = function(allowReconnect) { + if (!allowReconnect) this.stopReconnection(); + if (!this.socket) return; + this.socket.close(); + delete this.socket; + delete this.protocol; + delete this.background; // This is not persisted when reconnecting to the web socket server + delete this.optimizeHMD; + delete this.focusedState; + if (this.connected) { + this.connected = false; + this.emit('disconnect'); + } + return true; +} + +BaseConnection.prototype.reconnect = function() { + if (this.connected) { + this.stopReconnection(); + } else { + this.disconnect(true); + this.connect(); + } +} + +BaseConnection.prototype.handleData = function(data) { + var message = JSON.parse(data); + + var messageEvent; + if (this.protocol === undefined) { + messageEvent = this.protocol = chooseProtocol(message); + this.protocolVersionVerified = true; + this.emit('ready'); + } else { + messageEvent = this.protocol(message); + } + this.emit(messageEvent.type, messageEvent); +} + +BaseConnection.prototype.connect = function() { + if (this.socket) return; + this.socket = this.setupSocket(); + return true; +} + +BaseConnection.prototype.send = function(data) { + this.socket.send(data); +} + +BaseConnection.prototype.reportFocus = function(state) { + if (!this.connected || this.focusedState === state) return; + this.focusedState = state; + this.emit(this.focusedState ? 'focus' : 'blur'); + if (this.protocol && this.protocol.sendFocused) { + this.protocol.sendFocused(this, this.focusedState); + } +} + +_.extend(BaseConnection.prototype, EventEmitter.prototype); +},{"../protocol":15,"events":21,"underscore":24}],4:[function(require,module,exports){ +var BaseConnection = module.exports = require('./base') + , _ = require('underscore'); + + +var BrowserConnection = module.exports = function(opts) { + BaseConnection.call(this, opts); + var connection = this; + this.on('ready', function() { connection.startFocusLoop(); }) + this.on('disconnect', function() { connection.stopFocusLoop(); }) +} + +_.extend(BrowserConnection.prototype, BaseConnection.prototype); + +BrowserConnection.__proto__ = BaseConnection; + +BrowserConnection.prototype.useSecure = function(){ + return location.protocol === 'https:' +} + +BrowserConnection.prototype.getScheme = function(){ + return this.useSecure() ? 'wss:' : 'ws:' +} + +BrowserConnection.prototype.getPort = function(){ + return this.useSecure() ? 6436 : 6437 +} + +BrowserConnection.prototype.setupSocket = function() { + var connection = this; + var socket = new WebSocket(this.getUrl()); + socket.onopen = function() { connection.handleOpen(); }; + socket.onclose = function(data) { connection.handleClose(data['code'], data['reason']); }; + socket.onmessage = function(message) { connection.handleData(message.data) }; + socket.onerror = function(error) { + + // attempt to degrade to ws: after one failed attempt for older Leap Service installations. + if (connection.useSecure() && connection.scheme === 'wss:'){ + connection.scheme = 'ws:'; + connection.port = 6437; + connection.disconnect(); + connection.connect(); + } + + }; + return socket; +} + +BrowserConnection.prototype.startFocusLoop = function() { + if (this.focusDetectorTimer) return; + var connection = this; + var propertyName = null; + if (typeof document.hidden !== "undefined") { + propertyName = "hidden"; + } else if (typeof document.mozHidden !== "undefined") { + propertyName = "mozHidden"; + } else if (typeof document.msHidden !== "undefined") { + propertyName = "msHidden"; + } else if (typeof document.webkitHidden !== "undefined") { + propertyName = "webkitHidden"; + } else { + propertyName = undefined; + } + + if (connection.windowVisible === undefined) { + connection.windowVisible = propertyName === undefined ? true : document[propertyName] === false; + } + + var focusListener = window.addEventListener('focus', function(e) { + connection.windowVisible = true; + updateFocusState(); + }); + + var blurListener = window.addEventListener('blur', function(e) { + connection.windowVisible = false; + updateFocusState(); + }); + + this.on('disconnect', function() { + window.removeEventListener('focus', focusListener); + window.removeEventListener('blur', blurListener); + }); + + var updateFocusState = function() { + var isVisible = propertyName === undefined ? true : document[propertyName] === false; + connection.reportFocus(isVisible && connection.windowVisible); + } + + // save 100ms when resuming focus + updateFocusState(); + + this.focusDetectorTimer = setInterval(updateFocusState, 100); +} + +BrowserConnection.prototype.stopFocusLoop = function() { + if (!this.focusDetectorTimer) return; + clearTimeout(this.focusDetectorTimer); + delete this.focusDetectorTimer; +} + +},{"./base":3,"underscore":24}],5:[function(require,module,exports){ +var process=require("__browserify_process");var Frame = require('./frame') + , Hand = require('./hand') + , Pointable = require('./pointable') + , Finger = require('./finger') + , CircularBuffer = require("./circular_buffer") + , Pipeline = require("./pipeline") + , EventEmitter = require('events').EventEmitter + , gestureListener = require('./gesture').gestureListener + , Dialog = require('./dialog') + , _ = require('underscore'); + +/** + * Constructs a Controller object. + * + * When creating a Controller object, you may optionally pass in options + * to set the host , set the port, enable gestures, or select the frame event type. + * + * ```javascript + * var controller = new Leap.Controller({ + * host: '127.0.0.1', + * port: 6437, + * enableGestures: true, + * frameEventName: 'animationFrame' + * }); + * ``` + * + * @class Controller + * @memberof Leap + * @classdesc + * The Controller class is your main interface to the Leap Motion Controller. + * + * Create an instance of this Controller class to access frames of tracking data + * and configuration information. Frame data can be polled at any time using the + * [Controller.frame]{@link Leap.Controller#frame}() function. Call frame() or frame(0) to get the most recent + * frame. Set the history parameter to a positive integer to access previous frames. + * A controller stores up to 60 frames in its frame history. + * + * Polling is an appropriate strategy for applications which already have an + * intrinsic update loop, such as a game. + * + * loopWhileDisconnected defaults to true, and maintains a 60FPS frame rate even when Leap Motion is not streaming + * data at that rate (such as no hands in frame). This is important for VR/WebGL apps which rely on rendering for + * regular visual updates, including from other input devices. Flipping this to false should be considered an + * optimization for very specific use-cases. + * + * + */ + + +var Controller = module.exports = function(opts) { + var inNode = (typeof(process) !== 'undefined' && process.versions && process.versions.node), + controller = this; + + opts = _.defaults(opts || {}, { + inNode: inNode + }); + + this.inNode = opts.inNode; + + opts = _.defaults(opts || {}, { + frameEventName: this.useAnimationLoop() ? 'animationFrame' : 'deviceFrame', + suppressAnimationLoop: !this.useAnimationLoop(), + loopWhileDisconnected: true, + useAllPlugins: false, + checkVersion: true + }); + + this.animationFrameRequested = false; + this.onAnimationFrame = function(timestamp) { + if (controller.lastConnectionFrame.valid){ + controller.emit('animationFrame', controller.lastConnectionFrame); + } + controller.emit('frameEnd', timestamp); + if ( + controller.loopWhileDisconnected && + ( ( controller.connection.focusedState !== false ) // loop while undefined, pre-ready. + || controller.connection.opts.background) ){ + window.requestAnimationFrame(controller.onAnimationFrame); + }else{ + controller.animationFrameRequested = false; + } + }; + this.suppressAnimationLoop = opts.suppressAnimationLoop; + this.loopWhileDisconnected = opts.loopWhileDisconnected; + this.frameEventName = opts.frameEventName; + this.useAllPlugins = opts.useAllPlugins; + this.history = new CircularBuffer(200); + this.lastFrame = Frame.Invalid; + this.lastValidFrame = Frame.Invalid; + this.lastConnectionFrame = Frame.Invalid; + this.accumulatedGestures = []; + this.checkVersion = opts.checkVersion; + if (opts.connectionType === undefined) { + this.connectionType = (this.inBrowser() ? require('./connection/browser') : require('./connection/node')); + } else { + this.connectionType = opts.connectionType; + } + this.connection = new this.connectionType(opts); + this.streamingCount = 0; + this.devices = {}; + this.plugins = {}; + this._pluginPipelineSteps = {}; + this._pluginExtendedMethods = {}; + if (opts.useAllPlugins) this.useRegisteredPlugins(); + this.setupFrameEvents(opts); + this.setupConnectionEvents(); + + this.startAnimationLoop(); // immediately when started +} + +Controller.prototype.gesture = function(type, cb) { + var creator = gestureListener(this, type); + if (cb !== undefined) { + creator.stop(cb); + } + return creator; +} + +/* + * @returns the controller + */ +Controller.prototype.setBackground = function(state) { + this.connection.setBackground(state); + return this; +} + +Controller.prototype.setOptimizeHMD = function(state) { + this.connection.setOptimizeHMD(state); + return this; +} + +Controller.prototype.inBrowser = function() { + return !this.inNode; +} + +Controller.prototype.useAnimationLoop = function() { + return this.inBrowser() && !this.inBackgroundPage(); +} + +Controller.prototype.inBackgroundPage = function(){ + // http://developer.chrome.com/extensions/extension#method-getBackgroundPage + return (typeof(chrome) !== "undefined") && + chrome.extension && + chrome.extension.getBackgroundPage && + (chrome.extension.getBackgroundPage() === window) +} + +/* + * @returns the controller + */ +Controller.prototype.connect = function() { + this.connection.connect(); + return this; +} + +Controller.prototype.streaming = function() { + return this.streamingCount > 0; +} + +Controller.prototype.connected = function() { + return !!this.connection.connected; +} + +Controller.prototype.startAnimationLoop = function(){ + if (!this.suppressAnimationLoop && !this.animationFrameRequested) { + this.animationFrameRequested = true; + window.requestAnimationFrame(this.onAnimationFrame); + } +} + +/* + * @returns the controller + */ +Controller.prototype.disconnect = function() { + this.connection.disconnect(); + return this; +} + +/** + * Returns a frame of tracking data from the Leap. + * + * Use the optional history parameter to specify which frame to retrieve. + * Call frame() or frame(0) to access the most recent frame; call frame(1) to + * access the previous frame, and so on. If you use a history value greater + * than the number of stored frames, then the controller returns an invalid frame. + * + * @method frame + * @memberof Leap.Controller.prototype + * @param {number} history The age of the frame to return, counting backwards from + * the most recent frame (0) into the past and up to the maximum age (59). + * @returns {Leap.Frame} The specified frame; or, if no history + * parameter is specified, the newest frame. If a frame is not available at + * the specified history position, an invalid Frame is returned. + **/ +Controller.prototype.frame = function(num) { + return this.history.get(num) || Frame.Invalid; +} + +Controller.prototype.loop = function(callback) { + if (callback) { + if (typeof callback === 'function'){ + this.on(this.frameEventName, callback); + }else{ + // callback is actually of the form: {eventName: callback} + this.setupFrameEvents(callback); + } + } + + return this.connect(); +} + +Controller.prototype.addStep = function(step) { + if (!this.pipeline) this.pipeline = new Pipeline(this); + this.pipeline.addStep(step); +} + +// this is run on every deviceFrame +Controller.prototype.processFrame = function(frame) { + if (frame.gestures) { + this.accumulatedGestures = this.accumulatedGestures.concat(frame.gestures); + } + // lastConnectionFrame is used by the animation loop + this.lastConnectionFrame = frame; + this.startAnimationLoop(); // Only has effect if loopWhileDisconnected: false + this.emit('deviceFrame', frame); +} + +// on a this.deviceEventName (usually 'animationFrame' in browsers), this emits a 'frame' +Controller.prototype.processFinishedFrame = function(frame) { + this.lastFrame = frame; + if (frame.valid) { + this.lastValidFrame = frame; + } + frame.controller = this; + frame.historyIdx = this.history.push(frame); + if (frame.gestures) { + frame.gestures = this.accumulatedGestures; + this.accumulatedGestures = []; + for (var gestureIdx = 0; gestureIdx != frame.gestures.length; gestureIdx++) { + this.emit("gesture", frame.gestures[gestureIdx], frame); + } + } + if (this.pipeline) { + frame = this.pipeline.run(frame); + if (!frame) frame = Frame.Invalid; + } + this.emit('frame', frame); + this.emitHandEvents(frame); +} + +/** + * The controller will emit 'hand' events for every hand on each frame. The hand in question will be passed + * to the event callback. + * + * @param frame + */ +Controller.prototype.emitHandEvents = function(frame){ + for (var i = 0; i < frame.hands.length; i++){ + this.emit('hand', frame.hands[i]); + } +} + +Controller.prototype.setupFrameEvents = function(opts){ + if (opts.frame){ + this.on('frame', opts.frame); + } + if (opts.hand){ + this.on('hand', opts.hand); + } +} + +/** + Controller events. The old 'deviceConnected' and 'deviceDisconnected' have been depricated - + use 'deviceStreaming' and 'deviceStopped' instead, except in the case of an unexpected disconnect. + + There are 4 pairs of device events recently added/changed: + -deviceAttached/deviceRemoved - called when a device's physical connection to the computer changes + -deviceStreaming/deviceStopped - called when a device is paused or resumed. + -streamingStarted/streamingStopped - called when there is/is no longer at least 1 streaming device. + Always comes after deviceStreaming. + + The first of all of the above event pairs is triggered as appropriate upon connection. All of + these events receives an argument with the most recent info about the device that triggered it. + These events will always be fired in the order they are listed here, with reverse ordering for the + matching shutdown call. (ie, deviceStreaming always comes after deviceAttached, and deviceStopped + will come before deviceRemoved). + + -deviceConnected/deviceDisconnected - These are considered deprecated and will be removed in + the next revision. In contrast to the other events and in keeping with it's original behavior, + it will only be fired when a device begins streaming AFTER a connection has been established. + It is not paired, and receives no device info. Nearly identical functionality to + streamingStarted/Stopped if you need to port. +*/ +Controller.prototype.setupConnectionEvents = function() { + var controller = this; + this.connection.on('frame', function(frame) { + controller.processFrame(frame); + }); + // either deviceFrame or animationFrame: + this.on(this.frameEventName, function(frame) { + controller.processFinishedFrame(frame); + }); + + + // here we backfill the 0.5.0 deviceEvents as best possible + // backfill begin streaming events + var backfillStreamingStartedEventsHandler = function(){ + if (controller.connection.opts.requestProtocolVersion < 5 && controller.streamingCount == 0){ + controller.streamingCount = 1; + var info = { + attached: true, + streaming: true, + type: 'unknown', + id: "Lx00000000000" + }; + controller.devices[info.id] = info; + + controller.emit('deviceAttached', info); + controller.emit('deviceStreaming', info); + controller.emit('streamingStarted', info); + controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler) + } + } + + var backfillStreamingStoppedEvents = function(){ + if (controller.streamingCount > 0) { + for (var deviceId in controller.devices){ + controller.emit('deviceStopped', controller.devices[deviceId]); + controller.emit('deviceRemoved', controller.devices[deviceId]); + } + // only emit streamingStopped once, with the last device + controller.emit('streamingStopped', controller.devices[deviceId]); + + controller.streamingCount = 0; + + for (var deviceId in controller.devices){ + delete controller.devices[deviceId]; + } + } + } + // Delegate connection events + this.connection.on('focus', function() { + + if ( controller.loopWhileDisconnected ){ + + controller.startAnimationLoop(); + + } + + controller.emit('focus'); + + }); + this.connection.on('blur', function() { controller.emit('blur') }); + this.connection.on('protocol', function(protocol) { + + protocol.on('beforeFrameCreated', function(frameData){ + controller.emit('beforeFrameCreated', frameData) + }); + + protocol.on('afterFrameCreated', function(frame, frameData){ + controller.emit('afterFrameCreated', frame, frameData) + }); + + controller.emit('protocol', protocol); + }); + + this.connection.on('ready', function() { + + if (controller.checkVersion && !controller.inNode){ + // show dialog only to web users + controller.checkOutOfDate(); + } + + controller.emit('ready'); + }); + + this.connection.on('connect', function() { + controller.emit('connect'); + controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler) + controller.connection.on('frame', backfillStreamingStartedEventsHandler); + }); + + this.connection.on('disconnect', function() { + controller.emit('disconnect'); + backfillStreamingStoppedEvents(); + }); + + // this does not fire when the controller is manually disconnected + // or for Leap Service v1.2.0+ + this.connection.on('deviceConnect', function(evt) { + if (evt.state){ + controller.emit('deviceConnected'); + controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler) + controller.connection.on('frame', backfillStreamingStartedEventsHandler); + }else{ + controller.emit('deviceDisconnected'); + backfillStreamingStoppedEvents(); + } + }); + + // Does not fire for Leap Service pre v1.2.0 + this.connection.on('deviceEvent', function(evt) { + var info = evt.state, + oldInfo = controller.devices[info.id]; + + //Grab a list of changed properties in the device info + var changed = {}; + for(var property in info) { + //If a property i doesn't exist the cache, or has changed... + if( !oldInfo || !oldInfo.hasOwnProperty(property) || oldInfo[property] != info[property] ) { + changed[property] = true; + } + } + + //Update the device list + controller.devices[info.id] = info; + + //Fire events based on change list + if(changed.attached) { + controller.emit(info.attached ? 'deviceAttached' : 'deviceRemoved', info); + } + + if(!changed.streaming) return; + + if(info.streaming) { + controller.streamingCount++; + controller.emit('deviceStreaming', info); + if( controller.streamingCount == 1 ) { + controller.emit('streamingStarted', info); + } + //if attached & streaming both change to true at the same time, that device was streaming + //already when we connected. + if(!changed.attached) { + controller.emit('deviceConnected'); + } + } + //Since when devices are attached all fields have changed, don't send events for streaming being false. + else if(!(changed.attached && info.attached)) { + controller.streamingCount--; + controller.emit('deviceStopped', info); + if(controller.streamingCount == 0){ + controller.emit('streamingStopped', info); + } + controller.emit('deviceDisconnected'); + } + + }); + + + this.on('newListener', function(event, listener) { + if( event == 'deviceConnected' || event == 'deviceDisconnected' ) { + console.warn(event + " events are depricated. Consider using 'streamingStarted/streamingStopped' or 'deviceStreaming/deviceStopped' instead"); + } + }); + +}; + + + + +// Checks if the protocol version is the latest, if if not, shows the dialog. +Controller.prototype.checkOutOfDate = function(){ + console.assert(this.connection && this.connection.protocol); + + var serviceVersion = this.connection.protocol.serviceVersion; + var protocolVersion = this.connection.protocol.version; + var defaultProtocolVersion = this.connectionType.defaultProtocolVersion; + + if (defaultProtocolVersion > protocolVersion){ + + console.warn("Your Protocol Version is v" + protocolVersion + + ", this app was designed for v" + defaultProtocolVersion); + + Dialog.warnOutOfDate({ + sV: serviceVersion, + pV: protocolVersion + }); + return true + }else{ + return false + } + +}; + + + +Controller._pluginFactories = {}; + +/* + * Registers a plugin, making is accessible to controller.use later on. + * + * @member plugin + * @memberof Leap.Controller.prototype + * @param {String} name The name of the plugin (usually camelCase). + * @param {function} factory A factory method which will return an instance of a plugin. + * The factory receives an optional hash of options, passed in via controller.use. + * + * Valid keys for the object include frame, hand, finger, tool, and pointable. The value + * of each key can be either a function or an object. If given a function, that function + * will be called once for every instance of the object, with that instance injected as an + * argument. This allows decoration of objects with additional data: + * + * ```javascript + * Leap.Controller.plugin('testPlugin', function(options){ + * return { + * frame: function(frame){ + * frame.foo = 'bar'; + * } + * } + * }); + * ``` + * + * When hand is used, the callback is called for every hand in `frame.hands`. Note that + * hand objects are recreated with every new frame, so that data saved on the hand will not + * persist. + * + * ```javascript + * Leap.Controller.plugin('testPlugin', function(){ + * return { + * hand: function(hand){ + * console.log('testPlugin running on hand ' + hand.id); + * } + * } + * }); + * ``` + * + * A factory can return an object to add custom functionality to Frames, Hands, or Pointables. + * The methods are added directly to the object's prototype. Finger and Tool cannot be used here, Pointable + * must be used instead. + * This is encouraged for calculations which may not be necessary on every frame. + * Memoization is also encouraged, for cases where the method may be called many times per frame by the application. + * + * ```javascript + * // This plugin allows hand.usefulData() to be called later. + * Leap.Controller.plugin('testPlugin', function(){ + * return { + * hand: { + * usefulData: function(){ + * console.log('usefulData on hand', this.id); + * // memoize the results on to the hand, preventing repeat work: + * this.x || this.x = someExpensiveCalculation(); + * return this.x; + * } + * } + * } + * }); + * + * Note that the factory pattern allows encapsulation for every plugin instance. + * + * ```javascript + * Leap.Controller.plugin('testPlugin', function(options){ + * options || options = {} + * options.center || options.center = [0,0,0] + * + * privatePrintingMethod = function(){ + * console.log('privatePrintingMethod - options', options); + * } + * + * return { + * pointable: { + * publicPrintingMethod: function(){ + * privatePrintingMethod(); + * } + * } + * } + * }); + * + */ +Controller.plugin = function(pluginName, factory) { + if (this._pluginFactories[pluginName]) { + console.warn("Plugin \"" + pluginName + "\" already registered"); + } + return this._pluginFactories[pluginName] = factory; +}; + +/* + * Returns a list of registered plugins. + * @returns {Array} Plugin Factories. + */ +Controller.plugins = function() { + return _.keys(this._pluginFactories); +}; + + + +var setPluginCallbacks = function(pluginName, type, callback){ + + if ( ['beforeFrameCreated', 'afterFrameCreated'].indexOf(type) != -1 ){ + + // todo - not able to "unuse" a plugin currently + this.on(type, callback); + + }else { + + if (!this.pipeline) this.pipeline = new Pipeline(this); + + if (!this._pluginPipelineSteps[pluginName]) this._pluginPipelineSteps[pluginName] = []; + + this._pluginPipelineSteps[pluginName].push( + + this.pipeline.addWrappedStep(type, callback) + + ); + + } + +}; + +var setPluginMethods = function(pluginName, type, hash){ + var klass; + + if (!this._pluginExtendedMethods[pluginName]) this._pluginExtendedMethods[pluginName] = []; + + switch (type) { + case 'frame': + klass = Frame; + break; + case 'hand': + klass = Hand; + break; + case 'pointable': + klass = Pointable; + _.extend(Finger.prototype, hash); + _.extend(Finger.Invalid, hash); + break; + case 'finger': + klass = Finger; + break; + default: + throw pluginName + ' specifies invalid object type "' + type + '" for prototypical extension' + } + + _.extend(klass.prototype, hash); + _.extend(klass.Invalid, hash); + this._pluginExtendedMethods[pluginName].push([klass, hash]) + +} + + + +/* + * Begin using a registered plugin. The plugin's functionality will be added to all frames + * returned by the controller (and/or added to the objects within the frame). + * - The order of plugin execution inside the loop will match the order in which use is called by the application. + * - The plugin be run for both deviceFrames and animationFrames. + * + * If called a second time, the options will be merged with those of the already instantiated plugin. + * + * @method use + * @memberOf Leap.Controller.prototype + * @param pluginName + * @param {Hash} Options to be passed to the plugin's factory. + * @returns the controller + */ +Controller.prototype.use = function(pluginName, options) { + var functionOrHash, pluginFactory, key, pluginInstance; + + pluginFactory = (typeof pluginName == 'function') ? pluginName : Controller._pluginFactories[pluginName]; + + if (!pluginFactory) { + throw 'Leap Plugin ' + pluginName + ' not found.'; + } + + options || (options = {}); + + if (this.plugins[pluginName]){ + _.extend(this.plugins[pluginName], options); + return this; + } + + this.plugins[pluginName] = options; + + pluginInstance = pluginFactory.call(this, options); + + for (key in pluginInstance) { + + functionOrHash = pluginInstance[key]; + + if (typeof functionOrHash === 'function') { + + setPluginCallbacks.call(this, pluginName, key, functionOrHash); + + } else { + + setPluginMethods.call(this, pluginName, key, functionOrHash); + + } + + } + + return this; +}; + + + + +/* + * Stop using a used plugin. This will remove any of the plugin's pipeline methods (those called on every frame) + * and remove any methods which extend frame-object prototypes. + * + * @method stopUsing + * @memberOf Leap.Controller.prototype + * @param pluginName + * @returns the controller + */ +Controller.prototype.stopUsing = function (pluginName) { + var steps = this._pluginPipelineSteps[pluginName], + extMethodHashes = this._pluginExtendedMethods[pluginName], + i = 0, klass, extMethodHash; + + if (!this.plugins[pluginName]) return; + + if (steps) { + for (i = 0; i < steps.length; i++) { + this.pipeline.removeStep(steps[i]); + } + } + + if (extMethodHashes){ + for (i = 0; i < extMethodHashes.length; i++){ + klass = extMethodHashes[i][0]; + extMethodHash = extMethodHashes[i][1]; + for (var methodName in extMethodHash) { + delete klass.prototype[methodName]; + delete klass.Invalid[methodName]; + } + } + } + + delete this.plugins[pluginName]; + + return this; +} + +Controller.prototype.useRegisteredPlugins = function(){ + for (var plugin in Controller._pluginFactories){ + this.use(plugin); + } +} + + +_.extend(Controller.prototype, EventEmitter.prototype); + +},{"./circular_buffer":2,"./connection/browser":4,"./connection/node":20,"./dialog":6,"./finger":7,"./frame":8,"./gesture":9,"./hand":10,"./pipeline":13,"./pointable":14,"__browserify_process":22,"events":21,"underscore":24}],6:[function(require,module,exports){ +var process=require("__browserify_process");var Dialog = module.exports = function(message, options){ + this.options = (options || {}); + this.message = message; + + this.createElement(); +}; + +Dialog.prototype.createElement = function(){ + this.element = document.createElement('div'); + this.element.className = "leapjs-dialog"; + this.element.style.position = "fixed"; + this.element.style.top = '8px'; + this.element.style.left = 0; + this.element.style.right = 0; + this.element.style.textAlign = 'center'; + this.element.style.zIndex = 1000; + + var dialog = document.createElement('div'); + this.element.appendChild(dialog); + dialog.style.className = "leapjs-dialog"; + dialog.style.display = "inline-block"; + dialog.style.margin = "auto"; + dialog.style.padding = "8px"; + dialog.style.color = "#222"; + dialog.style.background = "#eee"; + dialog.style.borderRadius = "4px"; + dialog.style.border = "1px solid #999"; + dialog.style.textAlign = "left"; + dialog.style.cursor = "pointer"; + dialog.style.whiteSpace = "nowrap"; + dialog.style.transition = "box-shadow 1s linear"; + dialog.innerHTML = this.message; + + + if (this.options.onclick){ + dialog.addEventListener('click', this.options.onclick); + } + + if (this.options.onmouseover){ + dialog.addEventListener('mouseover', this.options.onmouseover); + } + + if (this.options.onmouseout){ + dialog.addEventListener('mouseout', this.options.onmouseout); + } + + if (this.options.onmousemove){ + dialog.addEventListener('mousemove', this.options.onmousemove); + } +}; + +Dialog.prototype.show = function(){ + document.body.appendChild(this.element); + return this; +}; + +Dialog.prototype.hide = function(){ + document.body.removeChild(this.element); + return this; +}; + + + + +// Shows a DOM dialog box with links to developer.leapmotion.com to upgrade +// This will work whether or not the Leap is plugged in, +// As long as it is called after a call to .connect() and the 'ready' event has fired. +Dialog.warnOutOfDate = function(params){ + params || (params = {}); + + var url = "http://developer.leapmotion.com?"; + + params.returnTo = window.location.href; + + for (var key in params){ + url += key + '=' + encodeURIComponent(params[key]) + '&'; + } + + var dialog, + onclick = function(event){ + + if (event.target.id != 'leapjs-decline-upgrade'){ + + var popup = window.open(url, + '_blank', + 'height=800,width=1000,location=1,menubar=1,resizable=1,status=1,toolbar=1,scrollbars=1' + ); + + if (window.focus) {popup.focus()} + + } + + dialog.hide(); + + return true; + }, + + + message = "This site requires Leap Motion Tracking V2." + + "<button id='leapjs-accept-upgrade' style='color: #444; transition: box-shadow 100ms linear; cursor: pointer; vertical-align: baseline; margin-left: 16px;'>Upgrade</button>" + + "<button id='leapjs-decline-upgrade' style='color: #444; transition: box-shadow 100ms linear; cursor: pointer; vertical-align: baseline; margin-left: 8px; '>Not Now</button>"; + + dialog = new Dialog(message, { + onclick: onclick, + onmousemove: function(e){ + if (e.target == document.getElementById('leapjs-decline-upgrade')){ + document.getElementById('leapjs-decline-upgrade').style.color = '#000'; + document.getElementById('leapjs-decline-upgrade').style.boxShadow = '0px 0px 2px #5daa00'; + + document.getElementById('leapjs-accept-upgrade').style.color = '#444'; + document.getElementById('leapjs-accept-upgrade').style.boxShadow = 'none'; + }else{ + document.getElementById('leapjs-accept-upgrade').style.color = '#000'; + document.getElementById('leapjs-accept-upgrade').style.boxShadow = '0px 0px 2px #5daa00'; + + document.getElementById('leapjs-decline-upgrade').style.color = '#444'; + document.getElementById('leapjs-decline-upgrade').style.boxShadow = 'none'; + } + }, + onmouseout: function(){ + document.getElementById('leapjs-decline-upgrade').style.color = '#444'; + document.getElementById('leapjs-decline-upgrade').style.boxShadow = 'none'; + document.getElementById('leapjs-accept-upgrade').style.color = '#444'; + document.getElementById('leapjs-accept-upgrade').style.boxShadow = 'none'; + } + } + ); + + return dialog.show(); +}; + + +// Tracks whether we've warned for lack of bones API. This will be shown only for early private-beta members. +Dialog.hasWarnedBones = false; + +Dialog.warnBones = function(){ + if (this.hasWarnedBones) return; + this.hasWarnedBones = true; + + console.warn("Your Leap Service is out of date"); + + if ( !(typeof(process) !== 'undefined' && process.versions && process.versions.node) ){ + this.warnOutOfDate({reason: 'bones'}); + } + +} +},{"__browserify_process":22}],7:[function(require,module,exports){ +var Pointable = require('./pointable'), + Bone = require('./bone') + , Dialog = require('./dialog') + , _ = require('underscore'); + +/** +* Constructs a Finger object. +* +* An uninitialized finger is considered invalid. +* Get valid Finger objects from a Frame or a Hand object. +* +* @class Finger +* @memberof Leap +* @classdesc +* The Finger class reports the physical characteristics of a finger. +* +* Both fingers and tools are classified as Pointable objects. Use the +* Pointable.tool property to determine whether a Pointable object represents a +* tool or finger. The Leap classifies a detected entity as a tool when it is +* thinner, straighter, and longer than a typical finger. +* +* Note that Finger objects can be invalid, which means that they do not +* contain valid tracking data and do not correspond to a physical entity. +* Invalid Finger objects can be the result of asking for a Finger object +* using an ID from an earlier frame when no Finger objects with that ID +* exist in the current frame. A Finger object created from the Finger +* constructor is also invalid. Test for validity with the Pointable.valid +* property. +*/ +var Finger = module.exports = function(data) { + Pointable.call(this, data); // use pointable as super-constructor + + /** + * The position of the distal interphalangeal joint of the finger. + * This joint is closest to the tip. + * + * The distal interphalangeal joint is located between the most extreme segment + * of the finger (the distal phalanx) and the middle segment (the medial + * phalanx). + * + * @member dipPosition + * @type {number[]} + * @memberof Leap.Finger.prototype + */ + this.dipPosition = data.dipPosition; + + /** + * The position of the proximal interphalangeal joint of the finger. This joint is the middle + * joint of a finger. + * + * The proximal interphalangeal joint is located between the two finger segments + * closest to the hand (the proximal and the medial phalanges). On a thumb, + * which lacks an medial phalanx, this joint index identifies the knuckle joint + * between the proximal phalanx and the metacarpal bone. + * + * @member pipPosition + * @type {number[]} + * @memberof Leap.Finger.prototype + */ + this.pipPosition = data.pipPosition; + + /** + * The position of the metacarpopophalangeal joint, or knuckle, of the finger. + * + * The metacarpopophalangeal joint is located at the base of a finger between + * the metacarpal bone and the first phalanx. The common name for this joint is + * the knuckle. + * + * On a thumb, which has one less phalanx than a finger, this joint index + * identifies the thumb joint near the base of the hand, between the carpal + * and metacarpal bones. + * + * @member mcpPosition + * @type {number[]} + * @memberof Leap.Finger.prototype + */ + this.mcpPosition = data.mcpPosition; + + /** + * The position of the Carpometacarpal joint + * + * This is at the distal end of the wrist, and has no common name. + * + */ + this.carpPosition = data.carpPosition; + + /** + * Whether or not this finger is in an extended posture. + * + * A finger is considered extended if it is extended straight from the hand as if + * pointing. A finger is not extended when it is bent down and curled towards the + * palm. + * @member extended + * @type {Boolean} + * @memberof Leap.Finger.prototype + */ + this.extended = data.extended; + + /** + * An integer code for the name of this finger. + * + * * 0 -- thumb + * * 1 -- index finger + * * 2 -- middle finger + * * 3 -- ring finger + * * 4 -- pinky + * + * @member type + * @type {number} + * @memberof Leap.Finger.prototype + */ + this.type = data.type; + + this.finger = true; + + /** + * The joint positions of this finger as an array in the order base to tip. + * + * @member positions + * @type {array[]} + * @memberof Leap.Finger.prototype + */ + this.positions = [this.carpPosition, this.mcpPosition, this.pipPosition, this.dipPosition, this.tipPosition]; + + if (data.bases){ + this.addBones(data); + } else { + Dialog.warnBones(); + } + +}; + +_.extend(Finger.prototype, Pointable.prototype); + + +Finger.prototype.addBones = function(data){ + /** + * Four bones per finger, from wrist outwards: + * metacarpal, proximal, medial, and distal. + * + * See http://en.wikipedia.org/wiki/Interphalangeal_articulations_of_hand + */ + this.metacarpal = new Bone(this, { + type: 0, + width: this.width, + prevJoint: this.carpPosition, + nextJoint: this.mcpPosition, + basis: data.bases[0] + }); + + this.proximal = new Bone(this, { + type: 1, + width: this.width, + prevJoint: this.mcpPosition, + nextJoint: this.pipPosition, + basis: data.bases[1] + }); + + this.medial = new Bone(this, { + type: 2, + width: this.width, + prevJoint: this.pipPosition, + nextJoint: this.dipPosition, + basis: data.bases[2] + }); + + /** + * Note that the `distal.nextJoint` position is slightly different from the `finger.tipPosition`. + * The former is at the very end of the bone, where the latter is the center of a sphere positioned at + * the tip of the finger. The btipPosition "bone tip position" is a few mm closer to the wrist than + * the tipPosition. + * @type {Bone} + */ + this.distal = new Bone(this, { + type: 3, + width: this.width, + prevJoint: this.dipPosition, + nextJoint: data.btipPosition, + basis: data.bases[3] + }); + + this.bones = [this.metacarpal, this.proximal, this.medial, this.distal]; +}; + +Finger.prototype.toString = function() { + return "Finger [ id:" + this.id + " " + this.length + "mmx | width:" + this.width + "mm | direction:" + this.direction + ' ]'; +}; + +Finger.Invalid = { valid: false }; + +},{"./bone":1,"./dialog":6,"./pointable":14,"underscore":24}],8:[function(require,module,exports){ +var Hand = require("./hand") + , Pointable = require("./pointable") + , createGesture = require("./gesture").createGesture + , glMatrix = require("gl-matrix") + , mat3 = glMatrix.mat3 + , vec3 = glMatrix.vec3 + , InteractionBox = require("./interaction_box") + , Finger = require('./finger') + , _ = require("underscore"); + +/** + * Constructs a Frame object. + * + * Frame instances created with this constructor are invalid. + * Get valid Frame objects by calling the + * [Controller.frame]{@link Leap.Controller#frame}() function. + *<C-D-Space> + * @class Frame + * @memberof Leap + * @classdesc + * The Frame class represents a set of hand and finger tracking data detected + * in a single frame. + * + * The Leap detects hands, fingers and tools within the tracking area, reporting + * their positions, orientations and motions in frames at the Leap frame rate. + * + * Access Frame objects using the [Controller.frame]{@link Leap.Controller#frame}() function. + */ +var Frame = module.exports = function(data) { + /** + * Reports whether this Frame instance is valid. + * + * A valid Frame is one generated by the Controller object that contains + * tracking data for all detected entities. An invalid Frame contains no + * actual tracking data, but you can call its functions without risk of a + * undefined object exception. The invalid Frame mechanism makes it more + * convenient to track individual data across the frame history. For example, + * you can invoke: + * + * ```javascript + * var finger = controller.frame(n).finger(fingerID); + * ``` + * + * for an arbitrary Frame history value, "n", without first checking whether + * frame(n) returned a null object. (You should still check that the + * returned Finger instance is valid.) + * + * @member valid + * @memberof Leap.Frame.prototype + * @type {Boolean} + */ + this.valid = true; + /** + * A unique ID for this Frame. Consecutive frames processed by the Leap + * have consecutive increasing values. + * @member id + * @memberof Leap.Frame.prototype + * @type {String} + */ + this.id = data.id; + /** + * The frame capture time in microseconds elapsed since the Leap started. + * @member timestamp + * @memberof Leap.Frame.prototype + * @type {number} + */ + this.timestamp = data.timestamp; + /** + * The list of Hand objects detected in this frame, given in arbitrary order. + * The list can be empty if no hands are detected. + * + * @member hands[] + * @memberof Leap.Frame.prototype + * @type {Leap.Hand} + */ + this.hands = []; + this.handsMap = {}; + /** + * The list of Pointable objects (fingers and tools) detected in this frame, + * given in arbitrary order. The list can be empty if no fingers or tools are + * detected. + * + * @member pointables[] + * @memberof Leap.Frame.prototype + * @type {Leap.Pointable} + */ + this.pointables = []; + /** + * The list of Tool objects detected in this frame, given in arbitrary order. + * The list can be empty if no tools are detected. + * + * @member tools[] + * @memberof Leap.Frame.prototype + * @type {Leap.Pointable} + */ + this.tools = []; + /** + * The list of Finger objects detected in this frame, given in arbitrary order. + * The list can be empty if no fingers are detected. + * @member fingers[] + * @memberof Leap.Frame.prototype + * @type {Leap.Pointable} + */ + this.fingers = []; + + /** + * The InteractionBox associated with the current frame. + * + * @member interactionBox + * @memberof Leap.Frame.prototype + * @type {Leap.InteractionBox} + */ + if (data.interactionBox) { + this.interactionBox = new InteractionBox(data.interactionBox); + } + this.gestures = []; + this.pointablesMap = {}; + this._translation = data.t; + this._rotation = _.flatten(data.r); + this._scaleFactor = data.s; + this.data = data; + this.type = 'frame'; // used by event emitting + this.currentFrameRate = data.currentFrameRate; + + if (data.gestures) { + /** + * The list of Gesture objects detected in this frame, given in arbitrary order. + * The list can be empty if no gestures are detected. + * + * Circle and swipe gestures are updated every frame. Tap gestures + * only appear in the list for a single frame. + * @member gestures[] + * @memberof Leap.Frame.prototype + * @type {Leap.Gesture} + */ + for (var gestureIdx = 0, gestureCount = data.gestures.length; gestureIdx != gestureCount; gestureIdx++) { + this.gestures.push(createGesture(data.gestures[gestureIdx])); + } + } + this.postprocessData(data); +}; + +Frame.prototype.postprocessData = function(data){ + if (!data) { + data = this.data; + } + + for (var handIdx = 0, handCount = data.hands.length; handIdx != handCount; handIdx++) { + var hand = new Hand(data.hands[handIdx]); + hand.frame = this; + this.hands.push(hand); + this.handsMap[hand.id] = hand; + } + + data.pointables = _.sortBy(data.pointables, function(pointable) { return pointable.id }); + + for (var pointableIdx = 0, pointableCount = data.pointables.length; pointableIdx != pointableCount; pointableIdx++) { + var pointableData = data.pointables[pointableIdx]; + var pointable = pointableData.dipPosition ? new Finger(pointableData) : new Pointable(pointableData); + pointable.frame = this; + this.addPointable(pointable); + } +}; + +/** + * Adds data from a pointable element into the pointablesMap; + * also adds the pointable to the frame.handsMap hand to which it belongs, + * and to the hand's tools or hand's fingers map. + * + * @param pointable {Object} a Pointable + */ +Frame.prototype.addPointable = function (pointable) { + this.pointables.push(pointable); + this.pointablesMap[pointable.id] = pointable; + (pointable.tool ? this.tools : this.fingers).push(pointable); + if (pointable.handId !== undefined && this.handsMap.hasOwnProperty(pointable.handId)) { + var hand = this.handsMap[pointable.handId]; + hand.pointables.push(pointable); + (pointable.tool ? hand.tools : hand.fingers).push(pointable); + switch (pointable.type){ + case 0: + hand.thumb = pointable; + break; + case 1: + hand.indexFinger = pointable; + break; + case 2: + hand.middleFinger = pointable; + break; + case 3: + hand.ringFinger = pointable; + break; + case 4: + hand.pinky = pointable; + break; + } + } +}; + +/** + * The tool with the specified ID in this frame. + * + * Use the Frame tool() function to retrieve a tool from + * this frame using an ID value obtained from a previous frame. + * This function always returns a Pointable object, but if no tool + * with the specified ID is present, an invalid Pointable object is returned. + * + * Note that ID values persist across frames, but only until tracking of a + * particular object is lost. If tracking of a tool is lost and subsequently + * regained, the new Pointable object representing that tool may have a + * different ID than that representing the tool in an earlier frame. + * + * @method tool + * @memberof Leap.Frame.prototype + * @param {String} id The ID value of a Tool object from a previous frame. + * @returns {Leap.Pointable} The tool with the + * matching ID if one exists in this frame; otherwise, an invalid Pointable object + * is returned. + */ +Frame.prototype.tool = function(id) { + var pointable = this.pointable(id); + return pointable.tool ? pointable : Pointable.Invalid; +}; + +/** + * The Pointable object with the specified ID in this frame. + * + * Use the Frame pointable() function to retrieve the Pointable object from + * this frame using an ID value obtained from a previous frame. + * This function always returns a Pointable object, but if no finger or tool + * with the specified ID is present, an invalid Pointable object is returned. + * + * Note that ID values persist across frames, but only until tracking of a + * particular object is lost. If tracking of a finger or tool is lost and subsequently + * regained, the new Pointable object representing that finger or tool may have + * a different ID than that representing the finger or tool in an earlier frame. + * + * @method pointable + * @memberof Leap.Frame.prototype + * @param {String} id The ID value of a Pointable object from a previous frame. + * @returns {Leap.Pointable} The Pointable object with + * the matching ID if one exists in this frame; + * otherwise, an invalid Pointable object is returned. + */ +Frame.prototype.pointable = function(id) { + return this.pointablesMap[id] || Pointable.Invalid; +}; + +/** + * The finger with the specified ID in this frame. + * + * Use the Frame finger() function to retrieve the finger from + * this frame using an ID value obtained from a previous frame. + * This function always returns a Finger object, but if no finger + * with the specified ID is present, an invalid Pointable object is returned. + * + * Note that ID values persist across frames, but only until tracking of a + * particular object is lost. If tracking of a finger is lost and subsequently + * regained, the new Pointable object representing that physical finger may have + * a different ID than that representing the finger in an earlier frame. + * + * @method finger + * @memberof Leap.Frame.prototype + * @param {String} id The ID value of a finger from a previous frame. + * @returns {Leap.Pointable} The finger with the + * matching ID if one exists in this frame; otherwise, an invalid Pointable + * object is returned. + */ +Frame.prototype.finger = function(id) { + var pointable = this.pointable(id); + return !pointable.tool ? pointable : Pointable.Invalid; +}; + +/** + * The Hand object with the specified ID in this frame. + * + * Use the Frame hand() function to retrieve the Hand object from + * this frame using an ID value obtained from a previous frame. + * This function always returns a Hand object, but if no hand + * with the specified ID is present, an invalid Hand object is returned. + * + * Note that ID values persist across frames, but only until tracking of a + * particular object is lost. If tracking of a hand is lost and subsequently + * regained, the new Hand object representing that physical hand may have + * a different ID than that representing the physical hand in an earlier frame. + * + * @method hand + * @memberof Leap.Frame.prototype + * @param {String} id The ID value of a Hand object from a previous frame. + * @returns {Leap.Hand} The Hand object with the matching + * ID if one exists in this frame; otherwise, an invalid Hand object is returned. + */ +Frame.prototype.hand = function(id) { + return this.handsMap[id] || Hand.Invalid; +}; + +/** + * The angle of rotation around the rotation axis derived from the overall + * rotational motion between the current frame and the specified frame. + * + * The returned angle is expressed in radians measured clockwise around + * the rotation axis (using the right-hand rule) between the start and end frames. + * The value is always between 0 and pi radians (0 and 180 degrees). + * + * The Leap derives frame rotation from the relative change in position and + * orientation of all objects detected in the field of view. + * + * If either this frame or sinceFrame is an invalid Frame object, then the + * angle of rotation is zero. + * + * @method rotationAngle + * @memberof Leap.Frame.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation. + * @param {number[]} [axis] The axis to measure rotation around. + * @returns {number} A positive value containing the heuristically determined + * rotational change between the current frame and that specified in the sinceFrame parameter. + */ +Frame.prototype.rotationAngle = function(sinceFrame, axis) { + if (!this.valid || !sinceFrame.valid) return 0.0; + + var rot = this.rotationMatrix(sinceFrame); + var cs = (rot[0] + rot[4] + rot[8] - 1.0)*0.5; + var angle = Math.acos(cs); + angle = isNaN(angle) ? 0.0 : angle; + + if (axis !== undefined) { + var rotAxis = this.rotationAxis(sinceFrame); + angle *= vec3.dot(rotAxis, vec3.normalize(vec3.create(), axis)); + } + + return angle; +}; + +/** + * The axis of rotation derived from the overall rotational motion between + * the current frame and the specified frame. + * + * The returned direction vector is normalized. + * + * The Leap derives frame rotation from the relative change in position and + * orientation of all objects detected in the field of view. + * + * If either this frame or sinceFrame is an invalid Frame object, or if no + * rotation is detected between the two frames, a zero vector is returned. + * + * @method rotationAxis + * @memberof Leap.Frame.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation. + * @returns {number[]} A normalized direction vector representing the axis of the heuristically determined + * rotational change between the current frame and that specified in the sinceFrame parameter. + */ +Frame.prototype.rotationAxis = function(sinceFrame) { + if (!this.valid || !sinceFrame.valid) return vec3.create(); + return vec3.normalize(vec3.create(), [ + this._rotation[7] - sinceFrame._rotation[5], + this._rotation[2] - sinceFrame._rotation[6], + this._rotation[3] - sinceFrame._rotation[1] + ]); +} + +/** + * The transform matrix expressing the rotation derived from the overall + * rotational motion between the current frame and the specified frame. + * + * The Leap derives frame rotation from the relative change in position and + * orientation of all objects detected in the field of view. + * + * If either this frame or sinceFrame is an invalid Frame object, then + * this method returns an identity matrix. + * + * @method rotationMatrix + * @memberof Leap.Frame.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation. + * @returns {number[]} A transformation matrix containing the heuristically determined + * rotational change between the current frame and that specified in the sinceFrame parameter. + */ +Frame.prototype.rotationMatrix = function(sinceFrame) { + if (!this.valid || !sinceFrame.valid) return mat3.create(); + var transpose = mat3.transpose(mat3.create(), this._rotation) + return mat3.multiply(mat3.create(), sinceFrame._rotation, transpose); +} + +/** + * The scale factor derived from the overall motion between the current frame and the specified frame. + * + * The scale factor is always positive. A value of 1.0 indicates no scaling took place. + * Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion. + * + * The Leap derives scaling from the relative inward or outward motion of all + * objects detected in the field of view (independent of translation and rotation). + * + * If either this frame or sinceFrame is an invalid Frame object, then this method returns 1.0. + * + * @method scaleFactor + * @memberof Leap.Frame.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative scaling. + * @returns {number} A positive value representing the heuristically determined + * scaling change ratio between the current frame and that specified in the sinceFrame parameter. + */ +Frame.prototype.scaleFactor = function(sinceFrame) { + if (!this.valid || !sinceFrame.valid) return 1.0; + return Math.exp(this._scaleFactor - sinceFrame._scaleFactor); +} + +/** + * The change of position derived from the overall linear motion between the + * current frame and the specified frame. + * + * The returned translation vector provides the magnitude and direction of the + * movement in millimeters. + * + * The Leap derives frame translation from the linear motion of all objects + * detected in the field of view. + * + * If either this frame or sinceFrame is an invalid Frame object, then this + * method returns a zero vector. + * + * @method translation + * @memberof Leap.Frame.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative translation. + * @returns {number[]} A vector representing the heuristically determined change in + * position of all objects between the current frame and that specified in the sinceFrame parameter. + */ +Frame.prototype.translation = function(sinceFrame) { + if (!this.valid || !sinceFrame.valid) return vec3.create(); + return vec3.subtract(vec3.create(), this._translation, sinceFrame._translation); +} + +/** + * A string containing a brief, human readable description of the Frame object. + * + * @method toString + * @memberof Leap.Frame.prototype + * @returns {String} A brief description of this frame. + */ +Frame.prototype.toString = function() { + var str = "Frame [ id:"+this.id+" | timestamp:"+this.timestamp+" | Hand count:("+this.hands.length+") | Pointable count:("+this.pointables.length+")"; + if (this.gestures) str += " | Gesture count:("+this.gestures.length+")"; + str += " ]"; + return str; +} + +/** + * Returns a JSON-formatted string containing the hands, pointables and gestures + * in this frame. + * + * @method dump + * @memberof Leap.Frame.prototype + * @returns {String} A JSON-formatted string. + */ +Frame.prototype.dump = function() { + var out = ''; + out += "Frame Info:<br/>"; + out += this.toString(); + out += "<br/><br/>Hands:<br/>" + for (var handIdx = 0, handCount = this.hands.length; handIdx != handCount; handIdx++) { + out += " "+ this.hands[handIdx].toString() + "<br/>"; + } + out += "<br/><br/>Pointables:<br/>"; + for (var pointableIdx = 0, pointableCount = this.pointables.length; pointableIdx != pointableCount; pointableIdx++) { + out += " "+ this.pointables[pointableIdx].toString() + "<br/>"; + } + if (this.gestures) { + out += "<br/><br/>Gestures:<br/>"; + for (var gestureIdx = 0, gestureCount = this.gestures.length; gestureIdx != gestureCount; gestureIdx++) { + out += " "+ this.gestures[gestureIdx].toString() + "<br/>"; + } + } + out += "<br/><br/>Raw JSON:<br/>"; + out += JSON.stringify(this.data); + return out; +} + +/** + * An invalid Frame object. + * + * You can use this invalid Frame in comparisons testing + * whether a given Frame instance is valid or invalid. (You can also check the + * [Frame.valid]{@link Leap.Frame#valid} property.) + * + * @static + * @type {Leap.Frame} + * @name Invalid + * @memberof Leap.Frame + */ +Frame.Invalid = { + valid: false, + hands: [], + fingers: [], + tools: [], + gestures: [], + pointables: [], + pointable: function() { return Pointable.Invalid }, + finger: function() { return Pointable.Invalid }, + hand: function() { return Hand.Invalid }, + toString: function() { return "invalid frame" }, + dump: function() { return this.toString() }, + rotationAngle: function() { return 0.0; }, + rotationMatrix: function() { return mat3.create(); }, + rotationAxis: function() { return vec3.create(); }, + scaleFactor: function() { return 1.0; }, + translation: function() { return vec3.create(); } +}; + +},{"./finger":7,"./gesture":9,"./hand":10,"./interaction_box":12,"./pointable":14,"gl-matrix":23,"underscore":24}],9:[function(require,module,exports){ +var glMatrix = require("gl-matrix") + , vec3 = glMatrix.vec3 + , EventEmitter = require('events').EventEmitter + , _ = require('underscore'); + +/** + * Constructs a new Gesture object. + * + * An uninitialized Gesture object is considered invalid. Get valid instances + * of the Gesture class, which will be one of the Gesture subclasses, from a + * Frame object. + * + * @class Gesture + * @abstract + * @memberof Leap + * @classdesc + * The Gesture class represents a recognized movement by the user. + * + * The Leap watches the activity within its field of view for certain movement + * patterns typical of a user gesture or command. For example, a movement from side to + * side with the hand can indicate a swipe gesture, while a finger poking forward + * can indicate a screen tap gesture. + * + * When the Leap recognizes a gesture, it assigns an ID and adds a + * Gesture object to the frame gesture list. For continuous gestures, which + * occur over many frames, the Leap updates the gesture by adding + * a Gesture object having the same ID and updated properties in each + * subsequent frame. + * + * **Important:** Recognition for each type of gesture must be enabled; + * otherwise **no gestures are recognized or reported**. + * + * Subclasses of Gesture define the properties for the specific movement patterns + * recognized by the Leap. + * + * The Gesture subclasses for include: + * + * * CircleGesture -- A circular movement by a finger. + * * SwipeGesture -- A straight line movement by the hand with fingers extended. + * * ScreenTapGesture -- A forward tapping movement by a finger. + * * KeyTapGesture -- A downward tapping movement by a finger. + * + * Circle and swipe gestures are continuous and these objects can have a + * state of start, update, and stop. + * + * The screen tap gesture is a discrete gesture. The Leap only creates a single + * ScreenTapGesture object appears for each tap and it always has a stop state. + * + * Get valid Gesture instances from a Frame object. You can get a list of gestures + * from the Frame gestures array. You can also use the Frame gesture() method + * to find a gesture in the current frame using an ID value obtained in a + * previous frame. + * + * Gesture objects can be invalid. For example, when you get a gesture by ID + * using Frame.gesture(), and there is no gesture with that ID in the current + * frame, then gesture() returns an Invalid Gesture object (rather than a null + * value). Always check object validity in situations where a gesture might be + * invalid. + */ +var createGesture = exports.createGesture = function(data) { + var gesture; + switch (data.type) { + case 'circle': + gesture = new CircleGesture(data); + break; + case 'swipe': + gesture = new SwipeGesture(data); + break; + case 'screenTap': + gesture = new ScreenTapGesture(data); + break; + case 'keyTap': + gesture = new KeyTapGesture(data); + break; + default: + throw "unknown gesture type"; + } + + /** + * The gesture ID. + * + * All Gesture objects belonging to the same recognized movement share the + * same ID value. Use the ID value with the Frame::gesture() method to + * find updates related to this Gesture object in subsequent frames. + * + * @member id + * @memberof Leap.Gesture.prototype + * @type {number} + */ + gesture.id = data.id; + /** + * The list of hands associated with this Gesture, if any. + * + * If no hands are related to this gesture, the list is empty. + * + * @member handIds + * @memberof Leap.Gesture.prototype + * @type {Array} + */ + gesture.handIds = data.handIds.slice(); + /** + * The list of fingers and tools associated with this Gesture, if any. + * + * If no Pointable objects are related to this gesture, the list is empty. + * + * @member pointableIds + * @memberof Leap.Gesture.prototype + * @type {Array} + */ + gesture.pointableIds = data.pointableIds.slice(); + /** + * The elapsed duration of the recognized movement up to the + * frame containing this Gesture object, in microseconds. + * + * The duration reported for the first Gesture in the sequence (with the + * start state) will typically be a small positive number since + * the movement must progress far enough for the Leap to recognize it as + * an intentional gesture. + * + * @member duration + * @memberof Leap.Gesture.prototype + * @type {number} + */ + gesture.duration = data.duration; + /** + * The gesture ID. + * + * Recognized movements occur over time and have a beginning, a middle, + * and an end. The 'state()' attribute reports where in that sequence this + * Gesture object falls. + * + * Possible values for the state field are: + * + * * start + * * update + * * stop + * + * @member state + * @memberof Leap.Gesture.prototype + * @type {String} + */ + gesture.state = data.state; + /** + * The gesture type. + * + * Possible values for the type field are: + * + * * circle + * * swipe + * * screenTap + * * keyTap + * + * @member type + * @memberof Leap.Gesture.prototype + * @type {String} + */ + gesture.type = data.type; + return gesture; +} + +/* + * Returns a builder object, which uses method chaining for gesture callback binding. + */ +var gestureListener = exports.gestureListener = function(controller, type) { + var handlers = {}; + var gestureMap = {}; + + controller.on('gesture', function(gesture, frame) { + if (gesture.type == type) { + if (gesture.state == "start" || gesture.state == "stop") { + if (gestureMap[gesture.id] === undefined) { + var gestureTracker = new Gesture(gesture, frame); + gestureMap[gesture.id] = gestureTracker; + _.each(handlers, function(cb, name) { + gestureTracker.on(name, cb); + }); + } + } + gestureMap[gesture.id].update(gesture, frame); + if (gesture.state == "stop") { + delete gestureMap[gesture.id]; + } + } + }); + var builder = { + start: function(cb) { + handlers['start'] = cb; + return builder; + }, + stop: function(cb) { + handlers['stop'] = cb; + return builder; + }, + complete: function(cb) { + handlers['stop'] = cb; + return builder; + }, + update: function(cb) { + handlers['update'] = cb; + return builder; + } + } + return builder; +} + +var Gesture = exports.Gesture = function(gesture, frame) { + this.gestures = [gesture]; + this.frames = [frame]; +} + +Gesture.prototype.update = function(gesture, frame) { + this.lastGesture = gesture; + this.lastFrame = frame; + this.gestures.push(gesture); + this.frames.push(frame); + this.emit(gesture.state, this); +} + +Gesture.prototype.translation = function() { + return vec3.subtract(vec3.create(), this.lastGesture.startPosition, this.lastGesture.position); +} + +_.extend(Gesture.prototype, EventEmitter.prototype); + +/** + * Constructs a new CircleGesture object. + * + * An uninitialized CircleGesture object is considered invalid. Get valid instances + * of the CircleGesture class from a Frame object. + * + * @class CircleGesture + * @memberof Leap + * @augments Leap.Gesture + * @classdesc + * The CircleGesture classes represents a circular finger movement. + * + * A circle movement is recognized when the tip of a finger draws a circle + * within the Leap field of view. + * + *  + * + * Circle gestures are continuous. The CircleGesture objects for the gesture have + * three possible states: + * + * * start -- The circle gesture has just started. The movement has + * progressed far enough for the recognizer to classify it as a circle. + * * update -- The circle gesture is continuing. + * * stop -- The circle gesture is finished. + */ +var CircleGesture = function(data) { + /** + * The center point of the circle within the Leap frame of reference. + * + * @member center + * @memberof Leap.CircleGesture.prototype + * @type {number[]} + */ + this.center = data.center; + /** + * The normal vector for the circle being traced. + * + * If you draw the circle clockwise, the normal vector points in the same + * general direction as the pointable object drawing the circle. If you draw + * the circle counterclockwise, the normal points back toward the + * pointable. If the angle between the normal and the pointable object + * drawing the circle is less than 90 degrees, then the circle is clockwise. + * + * ```javascript + * var clockwiseness; + * if (circle.pointable.direction.angleTo(circle.normal) <= PI/4) { + * clockwiseness = "clockwise"; + * } + * else + * { + * clockwiseness = "counterclockwise"; + * } + * ``` + * + * @member normal + * @memberof Leap.CircleGesture.prototype + * @type {number[]} + */ + this.normal = data.normal; + /** + * The number of times the finger tip has traversed the circle. + * + * Progress is reported as a positive number of the number. For example, + * a progress value of .5 indicates that the finger has gone halfway + * around, while a value of 3 indicates that the finger has gone around + * the the circle three times. + * + * Progress starts where the circle gesture began. Since the circle + * must be partially formed before the Leap can recognize it, progress + * will be greater than zero when a circle gesture first appears in the + * frame. + * + * @member progress + * @memberof Leap.CircleGesture.prototype + * @type {number} + */ + this.progress = data.progress; + /** + * The radius of the circle in mm. + * + * @member radius + * @memberof Leap.CircleGesture.prototype + * @type {number} + */ + this.radius = data.radius; +} + +CircleGesture.prototype.toString = function() { + return "CircleGesture ["+JSON.stringify(this)+"]"; +} + +/** + * Constructs a new SwipeGesture object. + * + * An uninitialized SwipeGesture object is considered invalid. Get valid instances + * of the SwipeGesture class from a Frame object. + * + * @class SwipeGesture + * @memberof Leap + * @augments Leap.Gesture + * @classdesc + * The SwipeGesture class represents a swiping motion of a finger or tool. + * + *  + * + * Swipe gestures are continuous. + */ +var SwipeGesture = function(data) { + /** + * The starting position within the Leap frame of + * reference, in mm. + * + * @member startPosition + * @memberof Leap.SwipeGesture.prototype + * @type {number[]} + */ + this.startPosition = data.startPosition; + /** + * The current swipe position within the Leap frame of + * reference, in mm. + * + * @member position + * @memberof Leap.SwipeGesture.prototype + * @type {number[]} + */ + this.position = data.position; + /** + * The unit direction vector parallel to the swipe motion. + * + * You can compare the components of the vector to classify the swipe as + * appropriate for your application. For example, if you are using swipes + * for two dimensional scrolling, you can compare the x and y values to + * determine if the swipe is primarily horizontal or vertical. + * + * @member direction + * @memberof Leap.SwipeGesture.prototype + * @type {number[]} + */ + this.direction = data.direction; + /** + * The speed of the finger performing the swipe gesture in + * millimeters per second. + * + * @member speed + * @memberof Leap.SwipeGesture.prototype + * @type {number} + */ + this.speed = data.speed; +} + +SwipeGesture.prototype.toString = function() { + return "SwipeGesture ["+JSON.stringify(this)+"]"; +} + +/** + * Constructs a new ScreenTapGesture object. + * + * An uninitialized ScreenTapGesture object is considered invalid. Get valid instances + * of the ScreenTapGesture class from a Frame object. + * + * @class ScreenTapGesture + * @memberof Leap + * @augments Leap.Gesture + * @classdesc + * The ScreenTapGesture class represents a tapping gesture by a finger or tool. + * + * A screen tap gesture is recognized when the tip of a finger pokes forward + * and then springs back to approximately the original postion, as if + * tapping a vertical screen. The tapping finger must pause briefly before beginning the tap. + * + *  + * + * ScreenTap gestures are discrete. The ScreenTapGesture object representing a tap always + * has the state, STATE_STOP. Only one ScreenTapGesture object is created for each + * screen tap gesture recognized. + */ +var ScreenTapGesture = function(data) { + /** + * The position where the screen tap is registered. + * + * @member position + * @memberof Leap.ScreenTapGesture.prototype + * @type {number[]} + */ + this.position = data.position; + /** + * The direction of finger tip motion. + * + * @member direction + * @memberof Leap.ScreenTapGesture.prototype + * @type {number[]} + */ + this.direction = data.direction; + /** + * The progess value is always 1.0 for a screen tap gesture. + * + * @member progress + * @memberof Leap.ScreenTapGesture.prototype + * @type {number} + */ + this.progress = data.progress; +} + +ScreenTapGesture.prototype.toString = function() { + return "ScreenTapGesture ["+JSON.stringify(this)+"]"; +} + +/** + * Constructs a new KeyTapGesture object. + * + * An uninitialized KeyTapGesture object is considered invalid. Get valid instances + * of the KeyTapGesture class from a Frame object. + * + * @class KeyTapGesture + * @memberof Leap + * @augments Leap.Gesture + * @classdesc + * The KeyTapGesture class represents a tapping gesture by a finger or tool. + * + * A key tap gesture is recognized when the tip of a finger rotates down toward the + * palm and then springs back to approximately the original postion, as if + * tapping. The tapping finger must pause briefly before beginning the tap. + * + *  + * + * Key tap gestures are discrete. The KeyTapGesture object representing a tap always + * has the state, STATE_STOP. Only one KeyTapGesture object is created for each + * key tap gesture recognized. + */ +var KeyTapGesture = function(data) { + /** + * The position where the key tap is registered. + * + * @member position + * @memberof Leap.KeyTapGesture.prototype + * @type {number[]} + */ + this.position = data.position; + /** + * The direction of finger tip motion. + * + * @member direction + * @memberof Leap.KeyTapGesture.prototype + * @type {number[]} + */ + this.direction = data.direction; + /** + * The progess value is always 1.0 for a key tap gesture. + * + * @member progress + * @memberof Leap.KeyTapGesture.prototype + * @type {number} + */ + this.progress = data.progress; +} + +KeyTapGesture.prototype.toString = function() { + return "KeyTapGesture ["+JSON.stringify(this)+"]"; +} + +},{"events":21,"gl-matrix":23,"underscore":24}],10:[function(require,module,exports){ +var Pointable = require("./pointable") + , Bone = require('./bone') + , glMatrix = require("gl-matrix") + , mat3 = glMatrix.mat3 + , vec3 = glMatrix.vec3 + , _ = require("underscore"); + +/** + * Constructs a Hand object. + * + * An uninitialized hand is considered invalid. + * Get valid Hand objects from a Frame object. + * @class Hand + * @memberof Leap + * @classdesc + * The Hand class reports the physical characteristics of a detected hand. + * + * Hand tracking data includes a palm position and velocity; vectors for + * the palm normal and direction to the fingers; properties of a sphere fit + * to the hand; and lists of the attached fingers and tools. + * + * Note that Hand objects can be invalid, which means that they do not contain + * valid tracking data and do not correspond to a physical entity. Invalid Hand + * objects can be the result of asking for a Hand object using an ID from an + * earlier frame when no Hand objects with that ID exist in the current frame. + * A Hand object created from the Hand constructor is also invalid. + * Test for validity with the [Hand.valid]{@link Leap.Hand#valid} property. + */ +var Hand = module.exports = function(data) { + /** + * A unique ID assigned to this Hand object, whose value remains the same + * across consecutive frames while the tracked hand remains visible. If + * tracking is lost (for example, when a hand is occluded by another hand + * or when it is withdrawn from or reaches the edge of the Leap field of view), + * the Leap may assign a new ID when it detects the hand in a future frame. + * + * Use the ID value with the {@link Frame.hand}() function to find this + * Hand object in future frames. + * + * @member id + * @memberof Leap.Hand.prototype + * @type {String} + */ + this.id = data.id; + /** + * The center position of the palm in millimeters from the Leap origin. + * @member palmPosition + * @memberof Leap.Hand.prototype + * @type {number[]} + */ + this.palmPosition = data.palmPosition; + /** + * The direction from the palm position toward the fingers. + * + * The direction is expressed as a unit vector pointing in the same + * direction as the directed line from the palm position to the fingers. + * + * @member direction + * @memberof Leap.Hand.prototype + * @type {number[]} + */ + this.direction = data.direction; + /** + * The rate of change of the palm position in millimeters/second. + * + * @member palmVeclocity + * @memberof Leap.Hand.prototype + * @type {number[]} + */ + this.palmVelocity = data.palmVelocity; + /** + * The normal vector to the palm. If your hand is flat, this vector will + * point downward, or "out" of the front surface of your palm. + * + *  + * + * The direction is expressed as a unit vector pointing in the same + * direction as the palm normal (that is, a vector orthogonal to the palm). + * @member palmNormal + * @memberof Leap.Hand.prototype + * @type {number[]} + */ + this.palmNormal = data.palmNormal; + /** + * The center of a sphere fit to the curvature of this hand. + * + * This sphere is placed roughly as if the hand were holding a ball. + * + *  + * @member sphereCenter + * @memberof Leap.Hand.prototype + * @type {number[]} + */ + this.sphereCenter = data.sphereCenter; + /** + * The radius of a sphere fit to the curvature of this hand, in millimeters. + * + * This sphere is placed roughly as if the hand were holding a ball. Thus the + * size of the sphere decreases as the fingers are curled into a fist. + * + * @member sphereRadius + * @memberof Leap.Hand.prototype + * @type {number} + */ + this.sphereRadius = data.sphereRadius; + /** + * Reports whether this is a valid Hand object. + * + * @member valid + * @memberof Leap.Hand.prototype + * @type {boolean} + */ + this.valid = true; + /** + * The list of Pointable objects (fingers and tools) detected in this frame + * that are associated with this hand, given in arbitrary order. The list + * can be empty if no fingers or tools associated with this hand are detected. + * + * Use the {@link Pointable} tool property to determine + * whether or not an item in the list represents a tool or finger. + * You can also get only the tools using the Hand.tools[] list or + * only the fingers using the Hand.fingers[] list. + * + * @member pointables[] + * @memberof Leap.Hand.prototype + * @type {Leap.Pointable[]} + */ + this.pointables = []; + /** + * The list of fingers detected in this frame that are attached to + * this hand, given in arbitrary order. + * + * The list can be empty if no fingers attached to this hand are detected. + * + * @member fingers[] + * @memberof Leap.Hand.prototype + * @type {Leap.Pointable[]} + */ + this.fingers = []; + + if (data.armBasis){ + this.arm = new Bone(this, { + type: 4, + width: data.armWidth, + prevJoint: data.elbow, + nextJoint: data.wrist, + basis: data.armBasis + }); + }else{ + this.arm = null; + } + + /** + * The list of tools detected in this frame that are held by this + * hand, given in arbitrary order. + * + * The list can be empty if no tools held by this hand are detected. + * + * @member tools[] + * @memberof Leap.Hand.prototype + * @type {Leap.Pointable[]} + */ + this.tools = []; + this._translation = data.t; + this._rotation = _.flatten(data.r); + this._scaleFactor = data.s; + + /** + * Time the hand has been visible in seconds. + * + * @member timeVisible + * @memberof Leap.Hand.prototype + * @type {number} + */ + this.timeVisible = data.timeVisible; + + /** + * The palm position with stabalization + * @member stabilizedPalmPosition + * @memberof Leap.Hand.prototype + * @type {number[]} + */ + this.stabilizedPalmPosition = data.stabilizedPalmPosition; + + /** + * Reports whether this is a left or a right hand. + * + * @member type + * @type {String} + * @memberof Leap.Hand.prototype + */ + this.type = data.type; + this.grabStrength = data.grabStrength; + this.pinchStrength = data.pinchStrength; + this.confidence = data.confidence; +} + +/** + * The finger with the specified ID attached to this hand. + * + * Use this function to retrieve a Pointable object representing a finger + * attached to this hand using an ID value obtained from a previous frame. + * This function always returns a Pointable object, but if no finger + * with the specified ID is present, an invalid Pointable object is returned. + * + * Note that the ID values assigned to fingers persist across frames, but only + * until tracking of a particular finger is lost. If tracking of a finger is + * lost and subsequently regained, the new Finger object representing that + * finger may have a different ID than that representing the finger in an + * earlier frame. + * + * @method finger + * @memberof Leap.Hand.prototype + * @param {String} id The ID value of a finger from a previous frame. + * @returns {Leap.Pointable} The Finger object with + * the matching ID if one exists for this hand in this frame; otherwise, an + * invalid Finger object is returned. + */ +Hand.prototype.finger = function(id) { + var finger = this.frame.finger(id); + return (finger && (finger.handId == this.id)) ? finger : Pointable.Invalid; +} + +/** + * The angle of rotation around the rotation axis derived from the change in + * orientation of this hand, and any associated fingers and tools, between the + * current frame and the specified frame. + * + * The returned angle is expressed in radians measured clockwise around the + * rotation axis (using the right-hand rule) between the start and end frames. + * The value is always between 0 and pi radians (0 and 180 degrees). + * + * If a corresponding Hand object is not found in sinceFrame, or if either + * this frame or sinceFrame are invalid Frame objects, then the angle of rotation is zero. + * + * @method rotationAngle + * @memberof Leap.Hand.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation. + * @param {numnber[]} [axis] The axis to measure rotation around. + * @returns {number} A positive value representing the heuristically determined + * rotational change of the hand between the current frame and that specified in + * the sinceFrame parameter. + */ +Hand.prototype.rotationAngle = function(sinceFrame, axis) { + if (!this.valid || !sinceFrame.valid) return 0.0; + var sinceHand = sinceFrame.hand(this.id); + if(!sinceHand.valid) return 0.0; + var rot = this.rotationMatrix(sinceFrame); + var cs = (rot[0] + rot[4] + rot[8] - 1.0)*0.5 + var angle = Math.acos(cs); + angle = isNaN(angle) ? 0.0 : angle; + if (axis !== undefined) { + var rotAxis = this.rotationAxis(sinceFrame); + angle *= vec3.dot(rotAxis, vec3.normalize(vec3.create(), axis)); + } + return angle; +} + +/** + * The axis of rotation derived from the change in orientation of this hand, and + * any associated fingers and tools, between the current frame and the specified frame. + * + * The returned direction vector is normalized. + * + * If a corresponding Hand object is not found in sinceFrame, or if either + * this frame or sinceFrame are invalid Frame objects, then this method returns a zero vector. + * + * @method rotationAxis + * @memberof Leap.Hand.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation. + * @returns {number[]} A normalized direction Vector representing the axis of the heuristically determined + * rotational change of the hand between the current frame and that specified in the sinceFrame parameter. + */ +Hand.prototype.rotationAxis = function(sinceFrame) { + if (!this.valid || !sinceFrame.valid) return vec3.create(); + var sinceHand = sinceFrame.hand(this.id); + if (!sinceHand.valid) return vec3.create(); + return vec3.normalize(vec3.create(), [ + this._rotation[7] - sinceHand._rotation[5], + this._rotation[2] - sinceHand._rotation[6], + this._rotation[3] - sinceHand._rotation[1] + ]); +} + +/** + * The transform matrix expressing the rotation derived from the change in + * orientation of this hand, and any associated fingers and tools, between + * the current frame and the specified frame. + * + * If a corresponding Hand object is not found in sinceFrame, or if either + * this frame or sinceFrame are invalid Frame objects, then this method returns + * an identity matrix. + * + * @method rotationMatrix + * @memberof Leap.Hand.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation. + * @returns {number[]} A transformation Matrix containing the heuristically determined + * rotational change of the hand between the current frame and that specified in the sinceFrame parameter. + */ +Hand.prototype.rotationMatrix = function(sinceFrame) { + if (!this.valid || !sinceFrame.valid) return mat3.create(); + var sinceHand = sinceFrame.hand(this.id); + if(!sinceHand.valid) return mat3.create(); + var transpose = mat3.transpose(mat3.create(), this._rotation); + var m = mat3.multiply(mat3.create(), sinceHand._rotation, transpose); + return m; +} + +/** + * The scale factor derived from the hand's motion between the current frame and the specified frame. + * + * The scale factor is always positive. A value of 1.0 indicates no scaling took place. + * Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion. + * + * The Leap derives scaling from the relative inward or outward motion of a hand + * and its associated fingers and tools (independent of translation and rotation). + * + * If a corresponding Hand object is not found in sinceFrame, or if either this frame or sinceFrame + * are invalid Frame objects, then this method returns 1.0. + * + * @method scaleFactor + * @memberof Leap.Hand.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative scaling. + * @returns {number} A positive value representing the heuristically determined + * scaling change ratio of the hand between the current frame and that specified in the sinceFrame parameter. + */ +Hand.prototype.scaleFactor = function(sinceFrame) { + if (!this.valid || !sinceFrame.valid) return 1.0; + var sinceHand = sinceFrame.hand(this.id); + if(!sinceHand.valid) return 1.0; + + return Math.exp(this._scaleFactor - sinceHand._scaleFactor); +} + +/** + * The change of position of this hand between the current frame and the specified frame + * + * The returned translation vector provides the magnitude and direction of the + * movement in millimeters. + * + * If a corresponding Hand object is not found in sinceFrame, or if either this frame or + * sinceFrame are invalid Frame objects, then this method returns a zero vector. + * + * @method translation + * @memberof Leap.Hand.prototype + * @param {Leap.Frame} sinceFrame The starting frame for computing the relative translation. + * @returns {number[]} A Vector representing the heuristically determined change in hand + * position between the current frame and that specified in the sinceFrame parameter. + */ +Hand.prototype.translation = function(sinceFrame) { + if (!this.valid || !sinceFrame.valid) return vec3.create(); + var sinceHand = sinceFrame.hand(this.id); + if(!sinceHand.valid) return vec3.create(); + return [ + this._translation[0] - sinceHand._translation[0], + this._translation[1] - sinceHand._translation[1], + this._translation[2] - sinceHand._translation[2] + ]; +} + +/** + * A string containing a brief, human readable description of the Hand object. + * @method toString + * @memberof Leap.Hand.prototype + * @returns {String} A description of the Hand as a string. + */ +Hand.prototype.toString = function() { + return "Hand (" + this.type + ") [ id: "+ this.id + " | palm velocity:"+this.palmVelocity+" | sphere center:"+this.sphereCenter+" ] "; +} + +/** + * The pitch angle in radians. + * + * Pitch is the angle between the negative z-axis and the projection of + * the vector onto the y-z plane. In other words, pitch represents rotation + * around the x-axis. + * If the vector points upward, the returned angle is between 0 and pi radians + * (180 degrees); if it points downward, the angle is between 0 and -pi radians. + * + * @method pitch + * @memberof Leap.Hand.prototype + * @returns {number} The angle of this vector above or below the horizon (x-z plane). + * + */ +Hand.prototype.pitch = function() { + return Math.atan2(this.direction[1], -this.direction[2]); +} + +/** + * The yaw angle in radians. + * + * Yaw is the angle between the negative z-axis and the projection of + * the vector onto the x-z plane. In other words, yaw represents rotation + * around the y-axis. If the vector points to the right of the negative z-axis, + * then the returned angle is between 0 and pi radians (180 degrees); + * if it points to the left, the angle is between 0 and -pi radians. + * + * @method yaw + * @memberof Leap.Hand.prototype + * @returns {number} The angle of this vector to the right or left of the y-axis. + * + */ +Hand.prototype.yaw = function() { + return Math.atan2(this.direction[0], -this.direction[2]); +} + +/** + * The roll angle in radians. + * + * Roll is the angle between the y-axis and the projection of + * the vector onto the x-y plane. In other words, roll represents rotation + * around the z-axis. If the vector points to the left of the y-axis, + * then the returned angle is between 0 and pi radians (180 degrees); + * if it points to the right, the angle is between 0 and -pi radians. + * + * @method roll + * @memberof Leap.Hand.prototype + * @returns {number} The angle of this vector to the right or left of the y-axis. + * + */ +Hand.prototype.roll = function() { + return Math.atan2(this.palmNormal[0], -this.palmNormal[1]); +} + +/** + * An invalid Hand object. + * + * You can use an invalid Hand object in comparisons testing + * whether a given Hand instance is valid or invalid. (You can also use the + * Hand valid property.) + * + * @static + * @type {Leap.Hand} + * @name Invalid + * @memberof Leap.Hand + */ +Hand.Invalid = { + valid: false, + fingers: [], + tools: [], + pointables: [], + left: false, + pointable: function() { return Pointable.Invalid }, + finger: function() { return Pointable.Invalid }, + toString: function() { return "invalid frame" }, + dump: function() { return this.toString(); }, + rotationAngle: function() { return 0.0; }, + rotationMatrix: function() { return mat3.create(); }, + rotationAxis: function() { return vec3.create(); }, + scaleFactor: function() { return 1.0; }, + translation: function() { return vec3.create(); } +}; + +},{"./bone":1,"./pointable":14,"gl-matrix":23,"underscore":24}],11:[function(require,module,exports){ +/** + * Leap is the global namespace of the Leap API. + * @namespace Leap + */ +module.exports = { + Controller: require("./controller"), + Frame: require("./frame"), + Gesture: require("./gesture"), + Hand: require("./hand"), + Pointable: require("./pointable"), + Finger: require("./finger"), + InteractionBox: require("./interaction_box"), + CircularBuffer: require("./circular_buffer"), + UI: require("./ui"), + JSONProtocol: require("./protocol").JSONProtocol, + glMatrix: require("gl-matrix"), + mat3: require("gl-matrix").mat3, + vec3: require("gl-matrix").vec3, + loopController: undefined, + version: require('./version.js'), + + /** + * Expose utility libraries for convenience + * Use carefully - they may be subject to upgrade or removal in different versions of LeapJS. + * + */ + _: require('underscore'), + EventEmitter: require('events').EventEmitter, + + /** + * The Leap.loop() function passes a frame of Leap data to your + * callback function and then calls window.requestAnimationFrame() after + * executing your callback function. + * + * Leap.loop() sets up the Leap controller and WebSocket connection for you. + * You do not need to create your own controller when using this method. + * + * Your callback function is called on an interval determined by the client + * browser. Typically, this is on an interval of 60 frames/second. The most + * recent frame of Leap data is passed to your callback function. If the Leap + * is producing frames at a slower rate than the browser frame rate, the same + * frame of Leap data can be passed to your function in successive animation + * updates. + * + * As an alternative, you can create your own Controller object and use a + * {@link Controller#onFrame onFrame} callback to process the data at + * the frame rate of the Leap device. See {@link Controller} for an + * example. + * + * @method Leap.loop + * @param {function} callback A function called when the browser is ready to + * draw to the screen. The most recent {@link Frame} object is passed to + * your callback function. + * + * ```javascript + * Leap.loop( function( frame ) { + * // ... your code here + * }) + * ``` + */ + loop: function(opts, callback) { + if (opts && callback === undefined && ( ({}).toString.call(opts) === '[object Function]' ) ) { + callback = opts; + opts = {}; + } + + if (this.loopController) { + if (opts){ + this.loopController.setupFrameEvents(opts); + } + }else{ + this.loopController = new this.Controller(opts); + } + + this.loopController.loop(callback); + return this.loopController; + }, + + /* + * Convenience method for Leap.Controller.plugin + */ + plugin: function(name, options){ + this.Controller.plugin(name, options) + } +} + +},{"./circular_buffer":2,"./controller":5,"./finger":7,"./frame":8,"./gesture":9,"./hand":10,"./interaction_box":12,"./pointable":14,"./protocol":15,"./ui":16,"./version.js":19,"events":21,"gl-matrix":23,"underscore":24}],12:[function(require,module,exports){ +var glMatrix = require("gl-matrix") + , vec3 = glMatrix.vec3; + +/** + * Constructs a InteractionBox object. + * + * @class InteractionBox + * @memberof Leap + * @classdesc + * The InteractionBox class represents a box-shaped region completely within + * the field of view of the Leap Motion controller. + * + * The interaction box is an axis-aligned rectangular prism and provides + * normalized coordinates for hands, fingers, and tools within this box. + * The InteractionBox class can make it easier to map positions in the + * Leap Motion coordinate system to 2D or 3D coordinate systems used + * for application drawing. + * + *  + * + * The InteractionBox region is defined by a center and dimensions along the x, y, and z axes. + */ +var InteractionBox = module.exports = function(data) { + /** + * Indicates whether this is a valid InteractionBox object. + * + * @member valid + * @type {Boolean} + * @memberof Leap.InteractionBox.prototype + */ + this.valid = true; + /** + * The center of the InteractionBox in device coordinates (millimeters). + * This point is equidistant from all sides of the box. + * + * @member center + * @type {number[]} + * @memberof Leap.InteractionBox.prototype + */ + this.center = data.center; + + this.size = data.size; + /** + * The width of the InteractionBox in millimeters, measured along the x-axis. + * + * @member width + * @type {number} + * @memberof Leap.InteractionBox.prototype + */ + this.width = data.size[0]; + /** + * The height of the InteractionBox in millimeters, measured along the y-axis. + * + * @member height + * @type {number} + * @memberof Leap.InteractionBox.prototype + */ + this.height = data.size[1]; + /** + * The depth of the InteractionBox in millimeters, measured along the z-axis. + * + * @member depth + * @type {number} + * @memberof Leap.InteractionBox.prototype + */ + this.depth = data.size[2]; +} + +/** + * Converts a position defined by normalized InteractionBox coordinates + * into device coordinates in millimeters. + * + * This function performs the inverse of normalizePoint(). + * + * @method denormalizePoint + * @memberof Leap.InteractionBox.prototype + * @param {number[]} normalizedPosition The input position in InteractionBox coordinates. + * @returns {number[]} The corresponding denormalized position in device coordinates. + */ +InteractionBox.prototype.denormalizePoint = function(normalizedPosition) { + return vec3.fromValues( + (normalizedPosition[0] - 0.5) * this.size[0] + this.center[0], + (normalizedPosition[1] - 0.5) * this.size[1] + this.center[1], + (normalizedPosition[2] - 0.5) * this.size[2] + this.center[2] + ); +} + +/** + * Normalizes the coordinates of a point using the interaction box. + * + * Coordinates from the Leap Motion frame of reference (millimeters) are + * converted to a range of [0..1] such that the minimum value of the + * InteractionBox maps to 0 and the maximum value of the InteractionBox maps to 1. + * + * @method normalizePoint + * @memberof Leap.InteractionBox.prototype + * @param {number[]} position The input position in device coordinates. + * @param {Boolean} clamp Whether or not to limit the output value to the range [0,1] + * when the input position is outside the InteractionBox. Defaults to true. + * @returns {number[]} The normalized position. + */ +InteractionBox.prototype.normalizePoint = function(position, clamp) { + var vec = vec3.fromValues( + ((position[0] - this.center[0]) / this.size[0]) + 0.5, + ((position[1] - this.center[1]) / this.size[1]) + 0.5, + ((position[2] - this.center[2]) / this.size[2]) + 0.5 + ); + + if (clamp) { + vec[0] = Math.min(Math.max(vec[0], 0), 1); + vec[1] = Math.min(Math.max(vec[1], 0), 1); + vec[2] = Math.min(Math.max(vec[2], 0), 1); + } + return vec; +} + +/** + * Writes a brief, human readable description of the InteractionBox object. + * + * @method toString + * @memberof Leap.InteractionBox.prototype + * @returns {String} A description of the InteractionBox object as a string. + */ +InteractionBox.prototype.toString = function() { + return "InteractionBox [ width:" + this.width + " | height:" + this.height + " | depth:" + this.depth + " ]"; +} + +/** + * An invalid InteractionBox object. + * + * You can use this InteractionBox instance in comparisons testing + * whether a given InteractionBox instance is valid or invalid. (You can also use the + * InteractionBox.valid property.) + * + * @static + * @type {Leap.InteractionBox} + * @name Invalid + * @memberof Leap.InteractionBox + */ +InteractionBox.Invalid = { valid: false }; + +},{"gl-matrix":23}],13:[function(require,module,exports){ +var Pipeline = module.exports = function (controller) { + this.steps = []; + this.controller = controller; +} + +Pipeline.prototype.addStep = function (step) { + this.steps.push(step); +} + +Pipeline.prototype.run = function (frame) { + var stepsLength = this.steps.length; + for (var i = 0; i != stepsLength; i++) { + if (!frame) break; + frame = this.steps[i](frame); + } + return frame; +} + +Pipeline.prototype.removeStep = function(step){ + var index = this.steps.indexOf(step); + if (index === -1) throw "Step not found in pipeline"; + this.steps.splice(index, 1); +} + +/* + * Wraps a plugin callback method in method which can be run inside the pipeline. + * This wrapper method loops the callback over objects within the frame as is appropriate, + * calling the callback for each in turn. + * + * @method createStepFunction + * @memberOf Leap.Controller.prototype + * @param {Controller} The controller on which the callback is called. + * @param {String} type What frame object the callback is run for and receives. + * Can be one of 'frame', 'finger', 'hand', 'pointable', 'tool' + * @param {function} callback The method which will be run inside the pipeline loop. Receives one argument, such as a hand. + * @private + */ +Pipeline.prototype.addWrappedStep = function (type, callback) { + var controller = this.controller, + step = function (frame) { + var dependencies, i, len; + dependencies = (type == 'frame') ? [frame] : (frame[type + 's'] || []); + + for (i = 0, len = dependencies.length; i < len; i++) { + callback.call(controller, dependencies[i]); + } + + return frame; + }; + + this.addStep(step); + return step; +}; +},{}],14:[function(require,module,exports){ +var glMatrix = require("gl-matrix") + , vec3 = glMatrix.vec3; + +/** + * Constructs a Pointable object. + * + * An uninitialized pointable is considered invalid. + * Get valid Pointable objects from a Frame or a Hand object. + * + * @class Pointable + * @memberof Leap + * @classdesc + * The Pointable class reports the physical characteristics of a detected + * finger or tool. + * + * Both fingers and tools are classified as Pointable objects. Use the + * Pointable.tool property to determine whether a Pointable object represents a + * tool or finger. The Leap classifies a detected entity as a tool when it is + * thinner, straighter, and longer than a typical finger. + * + * Note that Pointable objects can be invalid, which means that they do not + * contain valid tracking data and do not correspond to a physical entity. + * Invalid Pointable objects can be the result of asking for a Pointable object + * using an ID from an earlier frame when no Pointable objects with that ID + * exist in the current frame. A Pointable object created from the Pointable + * constructor is also invalid. Test for validity with the Pointable.valid + * property. + */ +var Pointable = module.exports = function(data) { + /** + * Indicates whether this is a valid Pointable object. + * + * @member valid + * @type {Boolean} + * @memberof Leap.Pointable.prototype + */ + this.valid = true; + /** + * A unique ID assigned to this Pointable object, whose value remains the + * same across consecutive frames while the tracked finger or tool remains + * visible. If tracking is lost (for example, when a finger is occluded by + * another finger or when it is withdrawn from the Leap field of view), the + * Leap may assign a new ID when it detects the entity in a future frame. + * + * Use the ID value with the pointable() functions defined for the + * {@link Frame} and {@link Frame.Hand} classes to find this + * Pointable object in future frames. + * + * @member id + * @type {String} + * @memberof Leap.Pointable.prototype + */ + this.id = data.id; + this.handId = data.handId; + /** + * The estimated length of the finger or tool in millimeters. + * + * The reported length is the visible length of the finger or tool from the + * hand to tip. If the length isn't known, then a value of 0 is returned. + * + * @member length + * @type {number} + * @memberof Leap.Pointable.prototype + */ + this.length = data.length; + /** + * Whether or not the Pointable is believed to be a tool. + * Tools are generally longer, thinner, and straighter than fingers. + * + * If tool is false, then this Pointable must be a finger. + * + * @member tool + * @type {Boolean} + * @memberof Leap.Pointable.prototype + */ + this.tool = data.tool; + /** + * The estimated width of the tool in millimeters. + * + * The reported width is the average width of the visible portion of the + * tool from the hand to the tip. If the width isn't known, + * then a value of 0 is returned. + * + * Pointable objects representing fingers do not have a width property. + * + * @member width + * @type {number} + * @memberof Leap.Pointable.prototype + */ + this.width = data.width; + /** + * The direction in which this finger or tool is pointing. + * + * The direction is expressed as a unit vector pointing in the same + * direction as the tip. + * + *  + * @member direction + * @type {number[]} + * @memberof Leap.Pointable.prototype + */ + this.direction = data.direction; + /** + * The tip position in millimeters from the Leap origin. + * Stabilized + * + * @member stabilizedTipPosition + * @type {number[]} + * @memberof Leap.Pointable.prototype + */ + this.stabilizedTipPosition = data.stabilizedTipPosition; + /** + * The tip position in millimeters from the Leap origin. + * + * @member tipPosition + * @type {number[]} + * @memberof Leap.Pointable.prototype + */ + this.tipPosition = data.tipPosition; + /** + * The rate of change of the tip position in millimeters/second. + * + * @member tipVelocity + * @type {number[]} + * @memberof Leap.Pointable.prototype + */ + this.tipVelocity = data.tipVelocity; + /** + * The current touch zone of this Pointable object. + * + * The Leap Motion software computes the touch zone based on a floating touch + * plane that adapts to the user's finger movement and hand posture. The Leap + * Motion software interprets purposeful movements toward this plane as potential touch + * points. When a Pointable moves close to the adaptive touch plane, it enters the + * "hovering" zone. When a Pointable reaches or passes through the plane, it enters + * the "touching" zone. + * + * The possible states include: + * + * * "none" -- The Pointable is outside the hovering zone. + * * "hovering" -- The Pointable is close to, but not touching the touch plane. + * * "touching" -- The Pointable has penetrated the touch plane. + * + * The touchDistance value provides a normalized indication of the distance to + * the touch plane when the Pointable is in the hovering or touching zones. + * + * @member touchZone + * @type {String} + * @memberof Leap.Pointable.prototype + */ + this.touchZone = data.touchZone; + /** + * A value proportional to the distance between this Pointable object and the + * adaptive touch plane. + * + *  + * + * The touch distance is a value in the range [-1, 1]. The value 1.0 indicates the + * Pointable is at the far edge of the hovering zone. The value 0 indicates the + * Pointable is just entering the touching zone. A value of -1.0 indicates the + * Pointable is firmly within the touching zone. Values in between are + * proportional to the distance from the plane. Thus, the touchDistance of 0.5 + * indicates that the Pointable is halfway into the hovering zone. + * + * You can use the touchDistance value to modulate visual feedback given to the + * user as their fingers close in on a touch target, such as a button. + * + * @member touchDistance + * @type {number} + * @memberof Leap.Pointable.prototype + */ + this.touchDistance = data.touchDistance; + + /** + * How long the pointable has been visible in seconds. + * + * @member timeVisible + * @type {number} + * @memberof Leap.Pointable.prototype + */ + this.timeVisible = data.timeVisible; +} + +/** + * A string containing a brief, human readable description of the Pointable + * object. + * + * @method toString + * @memberof Leap.Pointable.prototype + * @returns {String} A description of the Pointable object as a string. + */ +Pointable.prototype.toString = function() { + return "Pointable [ id:" + this.id + " " + this.length + "mmx | width:" + this.width + "mm | direction:" + this.direction + ' ]'; +} + +/** + * Returns the hand which the pointable is attached to. + */ +Pointable.prototype.hand = function(){ + return this.frame.hand(this.handId); +} + +/** + * An invalid Pointable object. + * + * You can use this Pointable instance in comparisons testing + * whether a given Pointable instance is valid or invalid. (You can also use the + * Pointable.valid property.) + + * @static + * @type {Leap.Pointable} + * @name Invalid + * @memberof Leap.Pointable + */ +Pointable.Invalid = { valid: false }; + +},{"gl-matrix":23}],15:[function(require,module,exports){ +var Frame = require('./frame') + , Hand = require('./hand') + , Pointable = require('./pointable') + , Finger = require('./finger') + , _ = require('underscore') + , EventEmitter = require('events').EventEmitter; + +var Event = function(data) { + this.type = data.type; + this.state = data.state; +}; + +exports.chooseProtocol = function(header) { + var protocol; + switch(header.version) { + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + protocol = JSONProtocol(header); + protocol.sendBackground = function(connection, state) { + connection.send(protocol.encode({background: state})); + } + protocol.sendFocused = function(connection, state) { + connection.send(protocol.encode({focused: state})); + } + protocol.sendOptimizeHMD = function(connection, state) { + connection.send(protocol.encode({optimizeHMD: state})); + } + break; + default: + throw "unrecognized version"; + } + return protocol; +} + +var JSONProtocol = exports.JSONProtocol = function(header) { + + var protocol = function(frameData) { + + if (frameData.event) { + + return new Event(frameData.event); + + } else { + + protocol.emit('beforeFrameCreated', frameData); + + var frame = new Frame(frameData); + + protocol.emit('afterFrameCreated', frame, frameData); + + return frame; + + } + + }; + + protocol.encode = function(message) { + return JSON.stringify(message); + }; + protocol.version = header.version; + protocol.serviceVersion = header.serviceVersion; + protocol.versionLong = 'Version ' + header.version; + protocol.type = 'protocol'; + + _.extend(protocol, EventEmitter.prototype); + + return protocol; +}; + + + +},{"./finger":7,"./frame":8,"./hand":10,"./pointable":14,"events":21,"underscore":24}],16:[function(require,module,exports){ +exports.UI = { + Region: require("./ui/region"), + Cursor: require("./ui/cursor") +}; +},{"./ui/cursor":17,"./ui/region":18}],17:[function(require,module,exports){ +var Cursor = module.exports = function() { + return function(frame) { + var pointable = frame.pointables.sort(function(a, b) { return a.z - b.z })[0] + if (pointable && pointable.valid) { + frame.cursorPosition = pointable.tipPosition + } + return frame + } +} + +},{}],18:[function(require,module,exports){ +var EventEmitter = require('events').EventEmitter + , _ = require('underscore') + +var Region = module.exports = function(start, end) { + this.start = new Vector(start) + this.end = new Vector(end) + this.enteredFrame = null +} + +Region.prototype.hasPointables = function(frame) { + for (var i = 0; i != frame.pointables.length; i++) { + var position = frame.pointables[i].tipPosition + if (position.x >= this.start.x && position.x <= this.end.x && position.y >= this.start.y && position.y <= this.end.y && position.z >= this.start.z && position.z <= this.end.z) { + return true + } + } + return false +} + +Region.prototype.listener = function(opts) { + var region = this + if (opts && opts.nearThreshold) this.setupNearRegion(opts.nearThreshold) + return function(frame) { + return region.updatePosition(frame) + } +} + +Region.prototype.clipper = function() { + var region = this + return function(frame) { + region.updatePosition(frame) + return region.enteredFrame ? frame : null + } +} + +Region.prototype.setupNearRegion = function(distance) { + var nearRegion = this.nearRegion = new Region( + [this.start.x - distance, this.start.y - distance, this.start.z - distance], + [this.end.x + distance, this.end.y + distance, this.end.z + distance] + ) + var region = this + nearRegion.on("enter", function(frame) { + region.emit("near", frame) + }) + nearRegion.on("exit", function(frame) { + region.emit("far", frame) + }) + region.on('exit', function(frame) { + region.emit("near", frame) + }) +} + +Region.prototype.updatePosition = function(frame) { + if (this.nearRegion) this.nearRegion.updatePosition(frame) + if (this.hasPointables(frame) && this.enteredFrame == null) { + this.enteredFrame = frame + this.emit("enter", this.enteredFrame) + } else if (!this.hasPointables(frame) && this.enteredFrame != null) { + this.enteredFrame = null + this.emit("exit", this.enteredFrame) + } + return frame +} + +Region.prototype.normalize = function(position) { + return new Vector([ + (position.x - this.start.x) / (this.end.x - this.start.x), + (position.y - this.start.y) / (this.end.y - this.start.y), + (position.z - this.start.z) / (this.end.z - this.start.z) + ]) +} + +Region.prototype.mapToXY = function(position, width, height) { + var normalized = this.normalize(position) + var x = normalized.x, y = normalized.y + if (x > 1) x = 1 + else if (x < -1) x = -1 + if (y > 1) y = 1 + else if (y < -1) y = -1 + return [ + (x + 1) / 2 * width, + (1 - y) / 2 * height, + normalized.z + ] +} + +_.extend(Region.prototype, EventEmitter.prototype) +},{"events":21,"underscore":24}],19:[function(require,module,exports){ +// This file is automatically updated from package.json by grunt. +module.exports = { + full: '0.6.4', + major: 0, + minor: 6, + dot: 4 +} +},{}],20:[function(require,module,exports){ + +},{}],21:[function(require,module,exports){ +var process=require("__browserify_process");if (!process.EventEmitter) process.EventEmitter = function () {}; + +var EventEmitter = exports.EventEmitter = process.EventEmitter; +var isArray = typeof Array.isArray === 'function' + ? Array.isArray + : function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]' + } +; +function indexOf (xs, x) { + if (xs.indexOf) return xs.indexOf(x); + for (var i = 0; i < xs.length; i++) { + if (x === xs[i]) return i; + } + return -1; +} + +// By default EventEmitters will print a warning if more than +// 10 listeners are added to it. This is a useful default which +// helps finding memory leaks. +// +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +var defaultMaxListeners = 10; +EventEmitter.prototype.setMaxListeners = function(n) { + if (!this._events) this._events = {}; + this._events.maxListeners = n; +}; + + +EventEmitter.prototype.emit = function(type) { + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events || !this._events.error || + (isArray(this._events.error) && !this._events.error.length)) + { + if (arguments[1] instanceof Error) { + throw arguments[1]; // Unhandled 'error' event + } else { + throw new Error("Uncaught, unspecified 'error' event."); + } + return false; + } + } + + if (!this._events) return false; + var handler = this._events[type]; + if (!handler) return false; + + if (typeof handler == 'function') { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + var args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + return true; + + } else if (isArray(handler)) { + var args = Array.prototype.slice.call(arguments, 1); + + var listeners = handler.slice(); + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + return true; + + } else { + return false; + } +}; + +// EventEmitter is defined in src/node_events.cc +// EventEmitter.prototype.emit() is also defined there. +EventEmitter.prototype.addListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('addListener only takes instances of Function'); + } + + if (!this._events) this._events = {}; + + // To avoid recursion in the case that type == "newListeners"! Before + // adding it to the listeners, first emit "newListeners". + this.emit('newListener', type, listener); + + if (!this._events[type]) { + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + } else if (isArray(this._events[type])) { + + // Check for listener leak + if (!this._events[type].warned) { + var m; + if (this._events.maxListeners !== undefined) { + m = this._events.maxListeners; + } else { + m = defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + console.trace(); + } + } + + // If we've already got an array, just append. + this._events[type].push(listener); + } else { + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + var self = this; + self.on(type, function g() { + self.removeListener(type, g); + listener.apply(this, arguments); + }); + + return this; +}; + +EventEmitter.prototype.removeListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('removeListener only takes instances of Function'); + } + + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events || !this._events[type]) return this; + + var list = this._events[type]; + + if (isArray(list)) { + var i = indexOf(list, listener); + if (i < 0) return this; + list.splice(i, 1); + if (list.length == 0) + delete this._events[type]; + } else if (this._events[type] === listener) { + delete this._events[type]; + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + if (arguments.length === 0) { + this._events = {}; + return this; + } + + // does not use listeners(), so no side effect of creating _events[type] + if (type && this._events && this._events[type]) this._events[type] = null; + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + if (!this._events) this._events = {}; + if (!this._events[type]) this._events[type] = []; + if (!isArray(this._events[type])) { + this._events[type] = [this._events[type]]; + } + return this._events[type]; +}; + +EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) + ret = 0; + else if (typeof emitter._events[type] === 'function') + ret = 1; + else + ret = emitter._events[type].length; + return ret; +}; + +},{"__browserify_process":22}],22:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; + +process.nextTick = (function () { + var canSetImmediate = typeof window !== 'undefined' + && window.setImmediate; + var canPost = typeof window !== 'undefined' + && window.postMessage && window.addEventListener + ; + + if (canSetImmediate) { + return function (f) { return window.setImmediate(f) }; + } + + if (canPost) { + var queue = []; + window.addEventListener('message', function (ev) { + var source = ev.source; + if ((source === window || source === null) && ev.data === 'process-tick') { + ev.stopPropagation(); + if (queue.length > 0) { + var fn = queue.shift(); + fn(); + } + } + }, true); + + return function nextTick(fn) { + queue.push(fn); + window.postMessage('process-tick', '*'); + }; + } + + return function nextTick(fn) { + setTimeout(fn, 0); + }; +})(); + +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +} + +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; + +},{}],23:[function(require,module,exports){ +/** + * @fileoverview gl-matrix - High performance matrix and vector operations + * @author Brandon Jones + * @author Colin MacKenzie IV + * @version 2.2.1 + */ + +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + + +(function(_global) { + "use strict"; + + var shim = {}; + if (typeof(exports) === 'undefined') { + if(typeof define == 'function' && typeof define.amd == 'object' && define.amd) { + shim.exports = {}; + define(function() { + return shim.exports; + }); + } else { + // gl-matrix lives in a browser, define its namespaces in global + shim.exports = typeof(window) !== 'undefined' ? window : _global; + } + } + else { + // gl-matrix lives in commonjs, define its namespaces in exports + shim.exports = exports; + } + + (function(exports) { + /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + + +if(!GLMAT_EPSILON) { + var GLMAT_EPSILON = 0.000001; +} + +if(!GLMAT_ARRAY_TYPE) { + var GLMAT_ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array; +} + +if(!GLMAT_RANDOM) { + var GLMAT_RANDOM = Math.random; +} + +/** + * @class Common utilities + * @name glMatrix + */ +var glMatrix = {}; + +/** + * Sets the type of array used when creating new vectors and matricies + * + * @param {Type} type Array type, such as Float32Array or Array + */ +glMatrix.setMatrixArrayType = function(type) { + GLMAT_ARRAY_TYPE = type; +} + +if(typeof(exports) !== 'undefined') { + exports.glMatrix = glMatrix; +} + +var degree = Math.PI / 180; + +/** +* Convert Degree To Radian +* +* @param {Number} Angle in Degrees +*/ +glMatrix.toRadian = function(a){ + return a * degree; +} +; +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @class 2 Dimensional Vector + * @name vec2 + */ + +var vec2 = {}; + +/** + * Creates a new, empty vec2 + * + * @returns {vec2} a new 2D vector + */ +vec2.create = function() { + var out = new GLMAT_ARRAY_TYPE(2); + out[0] = 0; + out[1] = 0; + return out; +}; + +/** + * Creates a new vec2 initialized with values from an existing vector + * + * @param {vec2} a vector to clone + * @returns {vec2} a new 2D vector + */ +vec2.clone = function(a) { + var out = new GLMAT_ARRAY_TYPE(2); + out[0] = a[0]; + out[1] = a[1]; + return out; +}; + +/** + * Creates a new vec2 initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @returns {vec2} a new 2D vector + */ +vec2.fromValues = function(x, y) { + var out = new GLMAT_ARRAY_TYPE(2); + out[0] = x; + out[1] = y; + return out; +}; + +/** + * Copy the values from one vec2 to another + * + * @param {vec2} out the receiving vector + * @param {vec2} a the source vector + * @returns {vec2} out + */ +vec2.copy = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + return out; +}; + +/** + * Set the components of a vec2 to the given values + * + * @param {vec2} out the receiving vector + * @param {Number} x X component + * @param {Number} y Y component + * @returns {vec2} out + */ +vec2.set = function(out, x, y) { + out[0] = x; + out[1] = y; + return out; +}; + +/** + * Adds two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ +vec2.add = function(out, a, b) { + out[0] = a[0] + b[0]; + out[1] = a[1] + b[1]; + return out; +}; + +/** + * Subtracts vector b from vector a + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ +vec2.subtract = function(out, a, b) { + out[0] = a[0] - b[0]; + out[1] = a[1] - b[1]; + return out; +}; + +/** + * Alias for {@link vec2.subtract} + * @function + */ +vec2.sub = vec2.subtract; + +/** + * Multiplies two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ +vec2.multiply = function(out, a, b) { + out[0] = a[0] * b[0]; + out[1] = a[1] * b[1]; + return out; +}; + +/** + * Alias for {@link vec2.multiply} + * @function + */ +vec2.mul = vec2.multiply; + +/** + * Divides two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ +vec2.divide = function(out, a, b) { + out[0] = a[0] / b[0]; + out[1] = a[1] / b[1]; + return out; +}; + +/** + * Alias for {@link vec2.divide} + * @function + */ +vec2.div = vec2.divide; + +/** + * Returns the minimum of two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ +vec2.min = function(out, a, b) { + out[0] = Math.min(a[0], b[0]); + out[1] = Math.min(a[1], b[1]); + return out; +}; + +/** + * Returns the maximum of two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ +vec2.max = function(out, a, b) { + out[0] = Math.max(a[0], b[0]); + out[1] = Math.max(a[1], b[1]); + return out; +}; + +/** + * Scales a vec2 by a scalar number + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {vec2} out + */ +vec2.scale = function(out, a, b) { + out[0] = a[0] * b; + out[1] = a[1] * b; + return out; +}; + +/** + * Adds two vec2's after scaling the second operand by a scalar value + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @param {Number} scale the amount to scale b by before adding + * @returns {vec2} out + */ +vec2.scaleAndAdd = function(out, a, b, scale) { + out[0] = a[0] + (b[0] * scale); + out[1] = a[1] + (b[1] * scale); + return out; +}; + +/** + * Calculates the euclidian distance between two vec2's + * + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {Number} distance between a and b + */ +vec2.distance = function(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1]; + return Math.sqrt(x*x + y*y); +}; + +/** + * Alias for {@link vec2.distance} + * @function + */ +vec2.dist = vec2.distance; + +/** + * Calculates the squared euclidian distance between two vec2's + * + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {Number} squared distance between a and b + */ +vec2.squaredDistance = function(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1]; + return x*x + y*y; +}; + +/** + * Alias for {@link vec2.squaredDistance} + * @function + */ +vec2.sqrDist = vec2.squaredDistance; + +/** + * Calculates the length of a vec2 + * + * @param {vec2} a vector to calculate length of + * @returns {Number} length of a + */ +vec2.length = function (a) { + var x = a[0], + y = a[1]; + return Math.sqrt(x*x + y*y); +}; + +/** + * Alias for {@link vec2.length} + * @function + */ +vec2.len = vec2.length; + +/** + * Calculates the squared length of a vec2 + * + * @param {vec2} a vector to calculate squared length of + * @returns {Number} squared length of a + */ +vec2.squaredLength = function (a) { + var x = a[0], + y = a[1]; + return x*x + y*y; +}; + +/** + * Alias for {@link vec2.squaredLength} + * @function + */ +vec2.sqrLen = vec2.squaredLength; + +/** + * Negates the components of a vec2 + * + * @param {vec2} out the receiving vector + * @param {vec2} a vector to negate + * @returns {vec2} out + */ +vec2.negate = function(out, a) { + out[0] = -a[0]; + out[1] = -a[1]; + return out; +}; + +/** + * Normalize a vec2 + * + * @param {vec2} out the receiving vector + * @param {vec2} a vector to normalize + * @returns {vec2} out + */ +vec2.normalize = function(out, a) { + var x = a[0], + y = a[1]; + var len = x*x + y*y; + if (len > 0) { + //TODO: evaluate use of glm_invsqrt here? + len = 1 / Math.sqrt(len); + out[0] = a[0] * len; + out[1] = a[1] * len; + } + return out; +}; + +/** + * Calculates the dot product of two vec2's + * + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {Number} dot product of a and b + */ +vec2.dot = function (a, b) { + return a[0] * b[0] + a[1] * b[1]; +}; + +/** + * Computes the cross product of two vec2's + * Note that the cross product must by definition produce a 3D vector + * + * @param {vec3} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec3} out + */ +vec2.cross = function(out, a, b) { + var z = a[0] * b[1] - a[1] * b[0]; + out[0] = out[1] = 0; + out[2] = z; + return out; +}; + +/** + * Performs a linear interpolation between two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @param {Number} t interpolation amount between the two inputs + * @returns {vec2} out + */ +vec2.lerp = function (out, a, b, t) { + var ax = a[0], + ay = a[1]; + out[0] = ax + t * (b[0] - ax); + out[1] = ay + t * (b[1] - ay); + return out; +}; + +/** + * Generates a random vector with the given scale + * + * @param {vec2} out the receiving vector + * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned + * @returns {vec2} out + */ +vec2.random = function (out, scale) { + scale = scale || 1.0; + var r = GLMAT_RANDOM() * 2.0 * Math.PI; + out[0] = Math.cos(r) * scale; + out[1] = Math.sin(r) * scale; + return out; +}; + +/** + * Transforms the vec2 with a mat2 + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to transform + * @param {mat2} m matrix to transform with + * @returns {vec2} out + */ +vec2.transformMat2 = function(out, a, m) { + var x = a[0], + y = a[1]; + out[0] = m[0] * x + m[2] * y; + out[1] = m[1] * x + m[3] * y; + return out; +}; + +/** + * Transforms the vec2 with a mat2d + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to transform + * @param {mat2d} m matrix to transform with + * @returns {vec2} out + */ +vec2.transformMat2d = function(out, a, m) { + var x = a[0], + y = a[1]; + out[0] = m[0] * x + m[2] * y + m[4]; + out[1] = m[1] * x + m[3] * y + m[5]; + return out; +}; + +/** + * Transforms the vec2 with a mat3 + * 3rd vector component is implicitly '1' + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to transform + * @param {mat3} m matrix to transform with + * @returns {vec2} out + */ +vec2.transformMat3 = function(out, a, m) { + var x = a[0], + y = a[1]; + out[0] = m[0] * x + m[3] * y + m[6]; + out[1] = m[1] * x + m[4] * y + m[7]; + return out; +}; + +/** + * Transforms the vec2 with a mat4 + * 3rd vector component is implicitly '0' + * 4th vector component is implicitly '1' + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to transform + * @param {mat4} m matrix to transform with + * @returns {vec2} out + */ +vec2.transformMat4 = function(out, a, m) { + var x = a[0], + y = a[1]; + out[0] = m[0] * x + m[4] * y + m[12]; + out[1] = m[1] * x + m[5] * y + m[13]; + return out; +}; + +/** + * Perform some operation over an array of vec2s. + * + * @param {Array} a the array of vectors to iterate over + * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed + * @param {Number} offset Number of elements to skip at the beginning of the array + * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array + * @param {Function} fn Function to call for each vector in the array + * @param {Object} [arg] additional argument to pass to fn + * @returns {Array} a + * @function + */ +vec2.forEach = (function() { + var vec = vec2.create(); + + return function(a, stride, offset, count, fn, arg) { + var i, l; + if(!stride) { + stride = 2; + } + + if(!offset) { + offset = 0; + } + + if(count) { + l = Math.min((count * stride) + offset, a.length); + } else { + l = a.length; + } + + for(i = offset; i < l; i += stride) { + vec[0] = a[i]; vec[1] = a[i+1]; + fn(vec, vec, arg); + a[i] = vec[0]; a[i+1] = vec[1]; + } + + return a; + }; +})(); + +/** + * Returns a string representation of a vector + * + * @param {vec2} vec vector to represent as a string + * @returns {String} string representation of the vector + */ +vec2.str = function (a) { + return 'vec2(' + a[0] + ', ' + a[1] + ')'; +}; + +if(typeof(exports) !== 'undefined') { + exports.vec2 = vec2; +} +; +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @class 3 Dimensional Vector + * @name vec3 + */ + +var vec3 = {}; + +/** + * Creates a new, empty vec3 + * + * @returns {vec3} a new 3D vector + */ +vec3.create = function() { + var out = new GLMAT_ARRAY_TYPE(3); + out[0] = 0; + out[1] = 0; + out[2] = 0; + return out; +}; + +/** + * Creates a new vec3 initialized with values from an existing vector + * + * @param {vec3} a vector to clone + * @returns {vec3} a new 3D vector + */ +vec3.clone = function(a) { + var out = new GLMAT_ARRAY_TYPE(3); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + return out; +}; + +/** + * Creates a new vec3 initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @returns {vec3} a new 3D vector + */ +vec3.fromValues = function(x, y, z) { + var out = new GLMAT_ARRAY_TYPE(3); + out[0] = x; + out[1] = y; + out[2] = z; + return out; +}; + +/** + * Copy the values from one vec3 to another + * + * @param {vec3} out the receiving vector + * @param {vec3} a the source vector + * @returns {vec3} out + */ +vec3.copy = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + return out; +}; + +/** + * Set the components of a vec3 to the given values + * + * @param {vec3} out the receiving vector + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @returns {vec3} out + */ +vec3.set = function(out, x, y, z) { + out[0] = x; + out[1] = y; + out[2] = z; + return out; +}; + +/** + * Adds two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +vec3.add = function(out, a, b) { + out[0] = a[0] + b[0]; + out[1] = a[1] + b[1]; + out[2] = a[2] + b[2]; + return out; +}; + +/** + * Subtracts vector b from vector a + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +vec3.subtract = function(out, a, b) { + out[0] = a[0] - b[0]; + out[1] = a[1] - b[1]; + out[2] = a[2] - b[2]; + return out; +}; + +/** + * Alias for {@link vec3.subtract} + * @function + */ +vec3.sub = vec3.subtract; + +/** + * Multiplies two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +vec3.multiply = function(out, a, b) { + out[0] = a[0] * b[0]; + out[1] = a[1] * b[1]; + out[2] = a[2] * b[2]; + return out; +}; + +/** + * Alias for {@link vec3.multiply} + * @function + */ +vec3.mul = vec3.multiply; + +/** + * Divides two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +vec3.divide = function(out, a, b) { + out[0] = a[0] / b[0]; + out[1] = a[1] / b[1]; + out[2] = a[2] / b[2]; + return out; +}; + +/** + * Alias for {@link vec3.divide} + * @function + */ +vec3.div = vec3.divide; + +/** + * Returns the minimum of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +vec3.min = function(out, a, b) { + out[0] = Math.min(a[0], b[0]); + out[1] = Math.min(a[1], b[1]); + out[2] = Math.min(a[2], b[2]); + return out; +}; + +/** + * Returns the maximum of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +vec3.max = function(out, a, b) { + out[0] = Math.max(a[0], b[0]); + out[1] = Math.max(a[1], b[1]); + out[2] = Math.max(a[2], b[2]); + return out; +}; + +/** + * Scales a vec3 by a scalar number + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {vec3} out + */ +vec3.scale = function(out, a, b) { + out[0] = a[0] * b; + out[1] = a[1] * b; + out[2] = a[2] * b; + return out; +}; + +/** + * Adds two vec3's after scaling the second operand by a scalar value + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @param {Number} scale the amount to scale b by before adding + * @returns {vec3} out + */ +vec3.scaleAndAdd = function(out, a, b, scale) { + out[0] = a[0] + (b[0] * scale); + out[1] = a[1] + (b[1] * scale); + out[2] = a[2] + (b[2] * scale); + return out; +}; + +/** + * Calculates the euclidian distance between two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} distance between a and b + */ +vec3.distance = function(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1], + z = b[2] - a[2]; + return Math.sqrt(x*x + y*y + z*z); +}; + +/** + * Alias for {@link vec3.distance} + * @function + */ +vec3.dist = vec3.distance; + +/** + * Calculates the squared euclidian distance between two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} squared distance between a and b + */ +vec3.squaredDistance = function(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1], + z = b[2] - a[2]; + return x*x + y*y + z*z; +}; + +/** + * Alias for {@link vec3.squaredDistance} + * @function + */ +vec3.sqrDist = vec3.squaredDistance; + +/** + * Calculates the length of a vec3 + * + * @param {vec3} a vector to calculate length of + * @returns {Number} length of a + */ +vec3.length = function (a) { + var x = a[0], + y = a[1], + z = a[2]; + return Math.sqrt(x*x + y*y + z*z); +}; + +/** + * Alias for {@link vec3.length} + * @function + */ +vec3.len = vec3.length; + +/** + * Calculates the squared length of a vec3 + * + * @param {vec3} a vector to calculate squared length of + * @returns {Number} squared length of a + */ +vec3.squaredLength = function (a) { + var x = a[0], + y = a[1], + z = a[2]; + return x*x + y*y + z*z; +}; + +/** + * Alias for {@link vec3.squaredLength} + * @function + */ +vec3.sqrLen = vec3.squaredLength; + +/** + * Negates the components of a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to negate + * @returns {vec3} out + */ +vec3.negate = function(out, a) { + out[0] = -a[0]; + out[1] = -a[1]; + out[2] = -a[2]; + return out; +}; + +/** + * Normalize a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to normalize + * @returns {vec3} out + */ +vec3.normalize = function(out, a) { + var x = a[0], + y = a[1], + z = a[2]; + var len = x*x + y*y + z*z; + if (len > 0) { + //TODO: evaluate use of glm_invsqrt here? + len = 1 / Math.sqrt(len); + out[0] = a[0] * len; + out[1] = a[1] * len; + out[2] = a[2] * len; + } + return out; +}; + +/** + * Calculates the dot product of two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} dot product of a and b + */ +vec3.dot = function (a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +}; + +/** + * Computes the cross product of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +vec3.cross = function(out, a, b) { + var ax = a[0], ay = a[1], az = a[2], + bx = b[0], by = b[1], bz = b[2]; + + out[0] = ay * bz - az * by; + out[1] = az * bx - ax * bz; + out[2] = ax * by - ay * bx; + return out; +}; + +/** + * Performs a linear interpolation between two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @param {Number} t interpolation amount between the two inputs + * @returns {vec3} out + */ +vec3.lerp = function (out, a, b, t) { + var ax = a[0], + ay = a[1], + az = a[2]; + out[0] = ax + t * (b[0] - ax); + out[1] = ay + t * (b[1] - ay); + out[2] = az + t * (b[2] - az); + return out; +}; + +/** + * Generates a random vector with the given scale + * + * @param {vec3} out the receiving vector + * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned + * @returns {vec3} out + */ +vec3.random = function (out, scale) { + scale = scale || 1.0; + + var r = GLMAT_RANDOM() * 2.0 * Math.PI; + var z = (GLMAT_RANDOM() * 2.0) - 1.0; + var zScale = Math.sqrt(1.0-z*z) * scale; + + out[0] = Math.cos(r) * zScale; + out[1] = Math.sin(r) * zScale; + out[2] = z * scale; + return out; +}; + +/** + * Transforms the vec3 with a mat4. + * 4th vector component is implicitly '1' + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {mat4} m matrix to transform with + * @returns {vec3} out + */ +vec3.transformMat4 = function(out, a, m) { + var x = a[0], y = a[1], z = a[2]; + out[0] = m[0] * x + m[4] * y + m[8] * z + m[12]; + out[1] = m[1] * x + m[5] * y + m[9] * z + m[13]; + out[2] = m[2] * x + m[6] * y + m[10] * z + m[14]; + return out; +}; + +/** + * Transforms the vec3 with a mat3. + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {mat4} m the 3x3 matrix to transform with + * @returns {vec3} out + */ +vec3.transformMat3 = function(out, a, m) { + var x = a[0], y = a[1], z = a[2]; + out[0] = x * m[0] + y * m[3] + z * m[6]; + out[1] = x * m[1] + y * m[4] + z * m[7]; + out[2] = x * m[2] + y * m[5] + z * m[8]; + return out; +}; + +/** + * Transforms the vec3 with a quat + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {quat} q quaternion to transform with + * @returns {vec3} out + */ +vec3.transformQuat = function(out, a, q) { + // benchmarks: http://jsperf.com/quaternion-transform-vec3-implementations + + var x = a[0], y = a[1], z = a[2], + qx = q[0], qy = q[1], qz = q[2], qw = q[3], + + // calculate quat * vec + ix = qw * x + qy * z - qz * y, + iy = qw * y + qz * x - qx * z, + iz = qw * z + qx * y - qy * x, + iw = -qx * x - qy * y - qz * z; + + // calculate result * inverse quat + out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; + out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; + out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; + return out; +}; + +/* +* Rotate a 3D vector around the x-axis +* @param {vec3} out The receiving vec3 +* @param {vec3} a The vec3 point to rotate +* @param {vec3} b The origin of the rotation +* @param {Number} c The angle of rotation +* @returns {vec3} out +*/ +vec3.rotateX = function(out, a, b, c){ + var p = [], r=[]; + //Translate point to the origin + p[0] = a[0] - b[0]; + p[1] = a[1] - b[1]; + p[2] = a[2] - b[2]; + + //perform rotation + r[0] = p[0]; + r[1] = p[1]*Math.cos(c) - p[2]*Math.sin(c); + r[2] = p[1]*Math.sin(c) + p[2]*Math.cos(c); + + //translate to correct position + out[0] = r[0] + b[0]; + out[1] = r[1] + b[1]; + out[2] = r[2] + b[2]; + + return out; +}; + +/* +* Rotate a 3D vector around the y-axis +* @param {vec3} out The receiving vec3 +* @param {vec3} a The vec3 point to rotate +* @param {vec3} b The origin of the rotation +* @param {Number} c The angle of rotation +* @returns {vec3} out +*/ +vec3.rotateY = function(out, a, b, c){ + var p = [], r=[]; + //Translate point to the origin + p[0] = a[0] - b[0]; + p[1] = a[1] - b[1]; + p[2] = a[2] - b[2]; + + //perform rotation + r[0] = p[2]*Math.sin(c) + p[0]*Math.cos(c); + r[1] = p[1]; + r[2] = p[2]*Math.cos(c) - p[0]*Math.sin(c); + + //translate to correct position + out[0] = r[0] + b[0]; + out[1] = r[1] + b[1]; + out[2] = r[2] + b[2]; + + return out; +}; + +/* +* Rotate a 3D vector around the z-axis +* @param {vec3} out The receiving vec3 +* @param {vec3} a The vec3 point to rotate +* @param {vec3} b The origin of the rotation +* @param {Number} c The angle of rotation +* @returns {vec3} out +*/ +vec3.rotateZ = function(out, a, b, c){ + var p = [], r=[]; + //Translate point to the origin + p[0] = a[0] - b[0]; + p[1] = a[1] - b[1]; + p[2] = a[2] - b[2]; + + //perform rotation + r[0] = p[0]*Math.cos(c) - p[1]*Math.sin(c); + r[1] = p[0]*Math.sin(c) + p[1]*Math.cos(c); + r[2] = p[2]; + + //translate to correct position + out[0] = r[0] + b[0]; + out[1] = r[1] + b[1]; + out[2] = r[2] + b[2]; + + return out; +}; + +/** + * Perform some operation over an array of vec3s. + * + * @param {Array} a the array of vectors to iterate over + * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed + * @param {Number} offset Number of elements to skip at the beginning of the array + * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array + * @param {Function} fn Function to call for each vector in the array + * @param {Object} [arg] additional argument to pass to fn + * @returns {Array} a + * @function + */ +vec3.forEach = (function() { + var vec = vec3.create(); + + return function(a, stride, offset, count, fn, arg) { + var i, l; + if(!stride) { + stride = 3; + } + + if(!offset) { + offset = 0; + } + + if(count) { + l = Math.min((count * stride) + offset, a.length); + } else { + l = a.length; + } + + for(i = offset; i < l; i += stride) { + vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; + fn(vec, vec, arg); + a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; + } + + return a; + }; +})(); + +/** + * Returns a string representation of a vector + * + * @param {vec3} vec vector to represent as a string + * @returns {String} string representation of the vector + */ +vec3.str = function (a) { + return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')'; +}; + +if(typeof(exports) !== 'undefined') { + exports.vec3 = vec3; +} +; +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @class 4 Dimensional Vector + * @name vec4 + */ + +var vec4 = {}; + +/** + * Creates a new, empty vec4 + * + * @returns {vec4} a new 4D vector + */ +vec4.create = function() { + var out = new GLMAT_ARRAY_TYPE(4); + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + return out; +}; + +/** + * Creates a new vec4 initialized with values from an existing vector + * + * @param {vec4} a vector to clone + * @returns {vec4} a new 4D vector + */ +vec4.clone = function(a) { + var out = new GLMAT_ARRAY_TYPE(4); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + return out; +}; + +/** + * Creates a new vec4 initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {vec4} a new 4D vector + */ +vec4.fromValues = function(x, y, z, w) { + var out = new GLMAT_ARRAY_TYPE(4); + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = w; + return out; +}; + +/** + * Copy the values from one vec4 to another + * + * @param {vec4} out the receiving vector + * @param {vec4} a the source vector + * @returns {vec4} out + */ +vec4.copy = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + return out; +}; + +/** + * Set the components of a vec4 to the given values + * + * @param {vec4} out the receiving vector + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {vec4} out + */ +vec4.set = function(out, x, y, z, w) { + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = w; + return out; +}; + +/** + * Adds two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +vec4.add = function(out, a, b) { + out[0] = a[0] + b[0]; + out[1] = a[1] + b[1]; + out[2] = a[2] + b[2]; + out[3] = a[3] + b[3]; + return out; +}; + +/** + * Subtracts vector b from vector a + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +vec4.subtract = function(out, a, b) { + out[0] = a[0] - b[0]; + out[1] = a[1] - b[1]; + out[2] = a[2] - b[2]; + out[3] = a[3] - b[3]; + return out; +}; + +/** + * Alias for {@link vec4.subtract} + * @function + */ +vec4.sub = vec4.subtract; + +/** + * Multiplies two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +vec4.multiply = function(out, a, b) { + out[0] = a[0] * b[0]; + out[1] = a[1] * b[1]; + out[2] = a[2] * b[2]; + out[3] = a[3] * b[3]; + return out; +}; + +/** + * Alias for {@link vec4.multiply} + * @function + */ +vec4.mul = vec4.multiply; + +/** + * Divides two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +vec4.divide = function(out, a, b) { + out[0] = a[0] / b[0]; + out[1] = a[1] / b[1]; + out[2] = a[2] / b[2]; + out[3] = a[3] / b[3]; + return out; +}; + +/** + * Alias for {@link vec4.divide} + * @function + */ +vec4.div = vec4.divide; + +/** + * Returns the minimum of two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +vec4.min = function(out, a, b) { + out[0] = Math.min(a[0], b[0]); + out[1] = Math.min(a[1], b[1]); + out[2] = Math.min(a[2], b[2]); + out[3] = Math.min(a[3], b[3]); + return out; +}; + +/** + * Returns the maximum of two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +vec4.max = function(out, a, b) { + out[0] = Math.max(a[0], b[0]); + out[1] = Math.max(a[1], b[1]); + out[2] = Math.max(a[2], b[2]); + out[3] = Math.max(a[3], b[3]); + return out; +}; + +/** + * Scales a vec4 by a scalar number + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {vec4} out + */ +vec4.scale = function(out, a, b) { + out[0] = a[0] * b; + out[1] = a[1] * b; + out[2] = a[2] * b; + out[3] = a[3] * b; + return out; +}; + +/** + * Adds two vec4's after scaling the second operand by a scalar value + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @param {Number} scale the amount to scale b by before adding + * @returns {vec4} out + */ +vec4.scaleAndAdd = function(out, a, b, scale) { + out[0] = a[0] + (b[0] * scale); + out[1] = a[1] + (b[1] * scale); + out[2] = a[2] + (b[2] * scale); + out[3] = a[3] + (b[3] * scale); + return out; +}; + +/** + * Calculates the euclidian distance between two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} distance between a and b + */ +vec4.distance = function(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1], + z = b[2] - a[2], + w = b[3] - a[3]; + return Math.sqrt(x*x + y*y + z*z + w*w); +}; + +/** + * Alias for {@link vec4.distance} + * @function + */ +vec4.dist = vec4.distance; + +/** + * Calculates the squared euclidian distance between two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} squared distance between a and b + */ +vec4.squaredDistance = function(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1], + z = b[2] - a[2], + w = b[3] - a[3]; + return x*x + y*y + z*z + w*w; +}; + +/** + * Alias for {@link vec4.squaredDistance} + * @function + */ +vec4.sqrDist = vec4.squaredDistance; + +/** + * Calculates the length of a vec4 + * + * @param {vec4} a vector to calculate length of + * @returns {Number} length of a + */ +vec4.length = function (a) { + var x = a[0], + y = a[1], + z = a[2], + w = a[3]; + return Math.sqrt(x*x + y*y + z*z + w*w); +}; + +/** + * Alias for {@link vec4.length} + * @function + */ +vec4.len = vec4.length; + +/** + * Calculates the squared length of a vec4 + * + * @param {vec4} a vector to calculate squared length of + * @returns {Number} squared length of a + */ +vec4.squaredLength = function (a) { + var x = a[0], + y = a[1], + z = a[2], + w = a[3]; + return x*x + y*y + z*z + w*w; +}; + +/** + * Alias for {@link vec4.squaredLength} + * @function + */ +vec4.sqrLen = vec4.squaredLength; + +/** + * Negates the components of a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to negate + * @returns {vec4} out + */ +vec4.negate = function(out, a) { + out[0] = -a[0]; + out[1] = -a[1]; + out[2] = -a[2]; + out[3] = -a[3]; + return out; +}; + +/** + * Normalize a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to normalize + * @returns {vec4} out + */ +vec4.normalize = function(out, a) { + var x = a[0], + y = a[1], + z = a[2], + w = a[3]; + var len = x*x + y*y + z*z + w*w; + if (len > 0) { + len = 1 / Math.sqrt(len); + out[0] = a[0] * len; + out[1] = a[1] * len; + out[2] = a[2] * len; + out[3] = a[3] * len; + } + return out; +}; + +/** + * Calculates the dot product of two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} dot product of a and b + */ +vec4.dot = function (a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; +}; + +/** + * Performs a linear interpolation between two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @param {Number} t interpolation amount between the two inputs + * @returns {vec4} out + */ +vec4.lerp = function (out, a, b, t) { + var ax = a[0], + ay = a[1], + az = a[2], + aw = a[3]; + out[0] = ax + t * (b[0] - ax); + out[1] = ay + t * (b[1] - ay); + out[2] = az + t * (b[2] - az); + out[3] = aw + t * (b[3] - aw); + return out; +}; + +/** + * Generates a random vector with the given scale + * + * @param {vec4} out the receiving vector + * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned + * @returns {vec4} out + */ +vec4.random = function (out, scale) { + scale = scale || 1.0; + + //TODO: This is a pretty awful way of doing this. Find something better. + out[0] = GLMAT_RANDOM(); + out[1] = GLMAT_RANDOM(); + out[2] = GLMAT_RANDOM(); + out[3] = GLMAT_RANDOM(); + vec4.normalize(out, out); + vec4.scale(out, out, scale); + return out; +}; + +/** + * Transforms the vec4 with a mat4. + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to transform + * @param {mat4} m matrix to transform with + * @returns {vec4} out + */ +vec4.transformMat4 = function(out, a, m) { + var x = a[0], y = a[1], z = a[2], w = a[3]; + out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; + out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; + out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; + out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; + return out; +}; + +/** + * Transforms the vec4 with a quat + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to transform + * @param {quat} q quaternion to transform with + * @returns {vec4} out + */ +vec4.transformQuat = function(out, a, q) { + var x = a[0], y = a[1], z = a[2], + qx = q[0], qy = q[1], qz = q[2], qw = q[3], + + // calculate quat * vec + ix = qw * x + qy * z - qz * y, + iy = qw * y + qz * x - qx * z, + iz = qw * z + qx * y - qy * x, + iw = -qx * x - qy * y - qz * z; + + // calculate result * inverse quat + out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; + out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; + out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; + return out; +}; + +/** + * Perform some operation over an array of vec4s. + * + * @param {Array} a the array of vectors to iterate over + * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed + * @param {Number} offset Number of elements to skip at the beginning of the array + * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array + * @param {Function} fn Function to call for each vector in the array + * @param {Object} [arg] additional argument to pass to fn + * @returns {Array} a + * @function + */ +vec4.forEach = (function() { + var vec = vec4.create(); + + return function(a, stride, offset, count, fn, arg) { + var i, l; + if(!stride) { + stride = 4; + } + + if(!offset) { + offset = 0; + } + + if(count) { + l = Math.min((count * stride) + offset, a.length); + } else { + l = a.length; + } + + for(i = offset; i < l; i += stride) { + vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; vec[3] = a[i+3]; + fn(vec, vec, arg); + a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; a[i+3] = vec[3]; + } + + return a; + }; +})(); + +/** + * Returns a string representation of a vector + * + * @param {vec4} vec vector to represent as a string + * @returns {String} string representation of the vector + */ +vec4.str = function (a) { + return 'vec4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')'; +}; + +if(typeof(exports) !== 'undefined') { + exports.vec4 = vec4; +} +; +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @class 2x2 Matrix + * @name mat2 + */ + +var mat2 = {}; + +/** + * Creates a new identity mat2 + * + * @returns {mat2} a new 2x2 matrix + */ +mat2.create = function() { + var out = new GLMAT_ARRAY_TYPE(4); + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; +}; + +/** + * Creates a new mat2 initialized with values from an existing matrix + * + * @param {mat2} a matrix to clone + * @returns {mat2} a new 2x2 matrix + */ +mat2.clone = function(a) { + var out = new GLMAT_ARRAY_TYPE(4); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + return out; +}; + +/** + * Copy the values from one mat2 to another + * + * @param {mat2} out the receiving matrix + * @param {mat2} a the source matrix + * @returns {mat2} out + */ +mat2.copy = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + return out; +}; + +/** + * Set a mat2 to the identity matrix + * + * @param {mat2} out the receiving matrix + * @returns {mat2} out + */ +mat2.identity = function(out) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; +}; + +/** + * Transpose the values of a mat2 + * + * @param {mat2} out the receiving matrix + * @param {mat2} a the source matrix + * @returns {mat2} out + */ +mat2.transpose = function(out, a) { + // If we are transposing ourselves we can skip a few steps but have to cache some values + if (out === a) { + var a1 = a[1]; + out[1] = a[2]; + out[2] = a1; + } else { + out[0] = a[0]; + out[1] = a[2]; + out[2] = a[1]; + out[3] = a[3]; + } + + return out; +}; + +/** + * Inverts a mat2 + * + * @param {mat2} out the receiving matrix + * @param {mat2} a the source matrix + * @returns {mat2} out + */ +mat2.invert = function(out, a) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], + + // Calculate the determinant + det = a0 * a3 - a2 * a1; + + if (!det) { + return null; + } + det = 1.0 / det; + + out[0] = a3 * det; + out[1] = -a1 * det; + out[2] = -a2 * det; + out[3] = a0 * det; + + return out; +}; + +/** + * Calculates the adjugate of a mat2 + * + * @param {mat2} out the receiving matrix + * @param {mat2} a the source matrix + * @returns {mat2} out + */ +mat2.adjoint = function(out, a) { + // Caching this value is nessecary if out == a + var a0 = a[0]; + out[0] = a[3]; + out[1] = -a[1]; + out[2] = -a[2]; + out[3] = a0; + + return out; +}; + +/** + * Calculates the determinant of a mat2 + * + * @param {mat2} a the source matrix + * @returns {Number} determinant of a + */ +mat2.determinant = function (a) { + return a[0] * a[3] - a[2] * a[1]; +}; + +/** + * Multiplies two mat2's + * + * @param {mat2} out the receiving matrix + * @param {mat2} a the first operand + * @param {mat2} b the second operand + * @returns {mat2} out + */ +mat2.multiply = function (out, a, b) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3]; + var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; + out[0] = a0 * b0 + a2 * b1; + out[1] = a1 * b0 + a3 * b1; + out[2] = a0 * b2 + a2 * b3; + out[3] = a1 * b2 + a3 * b3; + return out; +}; + +/** + * Alias for {@link mat2.multiply} + * @function + */ +mat2.mul = mat2.multiply; + +/** + * Rotates a mat2 by the given angle + * + * @param {mat2} out the receiving matrix + * @param {mat2} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat2} out + */ +mat2.rotate = function (out, a, rad) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], + s = Math.sin(rad), + c = Math.cos(rad); + out[0] = a0 * c + a2 * s; + out[1] = a1 * c + a3 * s; + out[2] = a0 * -s + a2 * c; + out[3] = a1 * -s + a3 * c; + return out; +}; + +/** + * Scales the mat2 by the dimensions in the given vec2 + * + * @param {mat2} out the receiving matrix + * @param {mat2} a the matrix to rotate + * @param {vec2} v the vec2 to scale the matrix by + * @returns {mat2} out + **/ +mat2.scale = function(out, a, v) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], + v0 = v[0], v1 = v[1]; + out[0] = a0 * v0; + out[1] = a1 * v0; + out[2] = a2 * v1; + out[3] = a3 * v1; + return out; +}; + +/** + * Returns a string representation of a mat2 + * + * @param {mat2} mat matrix to represent as a string + * @returns {String} string representation of the matrix + */ +mat2.str = function (a) { + return 'mat2(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')'; +}; + +/** + * Returns Frobenius norm of a mat2 + * + * @param {mat2} a the matrix to calculate Frobenius norm of + * @returns {Number} Frobenius norm + */ +mat2.frob = function (a) { + return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2))) +}; + +/** + * Returns L, D and U matrices (Lower triangular, Diagonal and Upper triangular) by factorizing the input matrix + * @param {mat2} L the lower triangular matrix + * @param {mat2} D the diagonal matrix + * @param {mat2} U the upper triangular matrix + * @param {mat2} a the input matrix to factorize + */ + +mat2.LDU = function (L, D, U, a) { + L[2] = a[2]/a[0]; + U[0] = a[0]; + U[1] = a[1]; + U[3] = a[3] - L[2] * U[1]; + return [L, D, U]; +}; + +if(typeof(exports) !== 'undefined') { + exports.mat2 = mat2; +} +; +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @class 2x3 Matrix + * @name mat2d + * + * @description + * A mat2d contains six elements defined as: + * <pre> + * [a, c, tx, + * b, d, ty] + * </pre> + * This is a short form for the 3x3 matrix: + * <pre> + * [a, c, tx, + * b, d, ty, + * 0, 0, 1] + * </pre> + * The last row is ignored so the array is shorter and operations are faster. + */ + +var mat2d = {}; + +/** + * Creates a new identity mat2d + * + * @returns {mat2d} a new 2x3 matrix + */ +mat2d.create = function() { + var out = new GLMAT_ARRAY_TYPE(6); + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 1; + out[4] = 0; + out[5] = 0; + return out; +}; + +/** + * Creates a new mat2d initialized with values from an existing matrix + * + * @param {mat2d} a matrix to clone + * @returns {mat2d} a new 2x3 matrix + */ +mat2d.clone = function(a) { + var out = new GLMAT_ARRAY_TYPE(6); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + return out; +}; + +/** + * Copy the values from one mat2d to another + * + * @param {mat2d} out the receiving matrix + * @param {mat2d} a the source matrix + * @returns {mat2d} out + */ +mat2d.copy = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + return out; +}; + +/** + * Set a mat2d to the identity matrix + * + * @param {mat2d} out the receiving matrix + * @returns {mat2d} out + */ +mat2d.identity = function(out) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 1; + out[4] = 0; + out[5] = 0; + return out; +}; + +/** + * Inverts a mat2d + * + * @param {mat2d} out the receiving matrix + * @param {mat2d} a the source matrix + * @returns {mat2d} out + */ +mat2d.invert = function(out, a) { + var aa = a[0], ab = a[1], ac = a[2], ad = a[3], + atx = a[4], aty = a[5]; + + var det = aa * ad - ab * ac; + if(!det){ + return null; + } + det = 1.0 / det; + + out[0] = ad * det; + out[1] = -ab * det; + out[2] = -ac * det; + out[3] = aa * det; + out[4] = (ac * aty - ad * atx) * det; + out[5] = (ab * atx - aa * aty) * det; + return out; +}; + +/** + * Calculates the determinant of a mat2d + * + * @param {mat2d} a the source matrix + * @returns {Number} determinant of a + */ +mat2d.determinant = function (a) { + return a[0] * a[3] - a[1] * a[2]; +}; + +/** + * Multiplies two mat2d's + * + * @param {mat2d} out the receiving matrix + * @param {mat2d} a the first operand + * @param {mat2d} b the second operand + * @returns {mat2d} out + */ +mat2d.multiply = function (out, a, b) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], + b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5]; + out[0] = a0 * b0 + a2 * b1; + out[1] = a1 * b0 + a3 * b1; + out[2] = a0 * b2 + a2 * b3; + out[3] = a1 * b2 + a3 * b3; + out[4] = a0 * b4 + a2 * b5 + a4; + out[5] = a1 * b4 + a3 * b5 + a5; + return out; +}; + +/** + * Alias for {@link mat2d.multiply} + * @function + */ +mat2d.mul = mat2d.multiply; + + +/** + * Rotates a mat2d by the given angle + * + * @param {mat2d} out the receiving matrix + * @param {mat2d} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat2d} out + */ +mat2d.rotate = function (out, a, rad) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], + s = Math.sin(rad), + c = Math.cos(rad); + out[0] = a0 * c + a2 * s; + out[1] = a1 * c + a3 * s; + out[2] = a0 * -s + a2 * c; + out[3] = a1 * -s + a3 * c; + out[4] = a4; + out[5] = a5; + return out; +}; + +/** + * Scales the mat2d by the dimensions in the given vec2 + * + * @param {mat2d} out the receiving matrix + * @param {mat2d} a the matrix to translate + * @param {vec2} v the vec2 to scale the matrix by + * @returns {mat2d} out + **/ +mat2d.scale = function(out, a, v) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], + v0 = v[0], v1 = v[1]; + out[0] = a0 * v0; + out[1] = a1 * v0; + out[2] = a2 * v1; + out[3] = a3 * v1; + out[4] = a4; + out[5] = a5; + return out; +}; + +/** + * Translates the mat2d by the dimensions in the given vec2 + * + * @param {mat2d} out the receiving matrix + * @param {mat2d} a the matrix to translate + * @param {vec2} v the vec2 to translate the matrix by + * @returns {mat2d} out + **/ +mat2d.translate = function(out, a, v) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], + v0 = v[0], v1 = v[1]; + out[0] = a0; + out[1] = a1; + out[2] = a2; + out[3] = a3; + out[4] = a0 * v0 + a2 * v1 + a4; + out[5] = a1 * v0 + a3 * v1 + a5; + return out; +}; + +/** + * Returns a string representation of a mat2d + * + * @param {mat2d} a matrix to represent as a string + * @returns {String} string representation of the matrix + */ +mat2d.str = function (a) { + return 'mat2d(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + + a[3] + ', ' + a[4] + ', ' + a[5] + ')'; +}; + +/** + * Returns Frobenius norm of a mat2d + * + * @param {mat2d} a the matrix to calculate Frobenius norm of + * @returns {Number} Frobenius norm + */ +mat2d.frob = function (a) { + return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + 1)) +}; + +if(typeof(exports) !== 'undefined') { + exports.mat2d = mat2d; +} +; +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @class 3x3 Matrix + * @name mat3 + */ + +var mat3 = {}; + +/** + * Creates a new identity mat3 + * + * @returns {mat3} a new 3x3 matrix + */ +mat3.create = function() { + var out = new GLMAT_ARRAY_TYPE(9); + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 1; + out[5] = 0; + out[6] = 0; + out[7] = 0; + out[8] = 1; + return out; +}; + +/** + * Copies the upper-left 3x3 values into the given mat3. + * + * @param {mat3} out the receiving 3x3 matrix + * @param {mat4} a the source 4x4 matrix + * @returns {mat3} out + */ +mat3.fromMat4 = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[4]; + out[4] = a[5]; + out[5] = a[6]; + out[6] = a[8]; + out[7] = a[9]; + out[8] = a[10]; + return out; +}; + +/** + * Creates a new mat3 initialized with values from an existing matrix + * + * @param {mat3} a matrix to clone + * @returns {mat3} a new 3x3 matrix + */ +mat3.clone = function(a) { + var out = new GLMAT_ARRAY_TYPE(9); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + return out; +}; + +/** + * Copy the values from one mat3 to another + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the source matrix + * @returns {mat3} out + */ +mat3.copy = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + return out; +}; + +/** + * Set a mat3 to the identity matrix + * + * @param {mat3} out the receiving matrix + * @returns {mat3} out + */ +mat3.identity = function(out) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 1; + out[5] = 0; + out[6] = 0; + out[7] = 0; + out[8] = 1; + return out; +}; + +/** + * Transpose the values of a mat3 + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the source matrix + * @returns {mat3} out + */ +mat3.transpose = function(out, a) { + // If we are transposing ourselves we can skip a few steps but have to cache some values + if (out === a) { + var a01 = a[1], a02 = a[2], a12 = a[5]; + out[1] = a[3]; + out[2] = a[6]; + out[3] = a01; + out[5] = a[7]; + out[6] = a02; + out[7] = a12; + } else { + out[0] = a[0]; + out[1] = a[3]; + out[2] = a[6]; + out[3] = a[1]; + out[4] = a[4]; + out[5] = a[7]; + out[6] = a[2]; + out[7] = a[5]; + out[8] = a[8]; + } + + return out; +}; + +/** + * Inverts a mat3 + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the source matrix + * @returns {mat3} out + */ +mat3.invert = function(out, a) { + var a00 = a[0], a01 = a[1], a02 = a[2], + a10 = a[3], a11 = a[4], a12 = a[5], + a20 = a[6], a21 = a[7], a22 = a[8], + + b01 = a22 * a11 - a12 * a21, + b11 = -a22 * a10 + a12 * a20, + b21 = a21 * a10 - a11 * a20, + + // Calculate the determinant + det = a00 * b01 + a01 * b11 + a02 * b21; + + if (!det) { + return null; + } + det = 1.0 / det; + + out[0] = b01 * det; + out[1] = (-a22 * a01 + a02 * a21) * det; + out[2] = (a12 * a01 - a02 * a11) * det; + out[3] = b11 * det; + out[4] = (a22 * a00 - a02 * a20) * det; + out[5] = (-a12 * a00 + a02 * a10) * det; + out[6] = b21 * det; + out[7] = (-a21 * a00 + a01 * a20) * det; + out[8] = (a11 * a00 - a01 * a10) * det; + return out; +}; + +/** + * Calculates the adjugate of a mat3 + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the source matrix + * @returns {mat3} out + */ +mat3.adjoint = function(out, a) { + var a00 = a[0], a01 = a[1], a02 = a[2], + a10 = a[3], a11 = a[4], a12 = a[5], + a20 = a[6], a21 = a[7], a22 = a[8]; + + out[0] = (a11 * a22 - a12 * a21); + out[1] = (a02 * a21 - a01 * a22); + out[2] = (a01 * a12 - a02 * a11); + out[3] = (a12 * a20 - a10 * a22); + out[4] = (a00 * a22 - a02 * a20); + out[5] = (a02 * a10 - a00 * a12); + out[6] = (a10 * a21 - a11 * a20); + out[7] = (a01 * a20 - a00 * a21); + out[8] = (a00 * a11 - a01 * a10); + return out; +}; + +/** + * Calculates the determinant of a mat3 + * + * @param {mat3} a the source matrix + * @returns {Number} determinant of a + */ +mat3.determinant = function (a) { + var a00 = a[0], a01 = a[1], a02 = a[2], + a10 = a[3], a11 = a[4], a12 = a[5], + a20 = a[6], a21 = a[7], a22 = a[8]; + + return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20); +}; + +/** + * Multiplies two mat3's + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the first operand + * @param {mat3} b the second operand + * @returns {mat3} out + */ +mat3.multiply = function (out, a, b) { + var a00 = a[0], a01 = a[1], a02 = a[2], + a10 = a[3], a11 = a[4], a12 = a[5], + a20 = a[6], a21 = a[7], a22 = a[8], + + b00 = b[0], b01 = b[1], b02 = b[2], + b10 = b[3], b11 = b[4], b12 = b[5], + b20 = b[6], b21 = b[7], b22 = b[8]; + + out[0] = b00 * a00 + b01 * a10 + b02 * a20; + out[1] = b00 * a01 + b01 * a11 + b02 * a21; + out[2] = b00 * a02 + b01 * a12 + b02 * a22; + + out[3] = b10 * a00 + b11 * a10 + b12 * a20; + out[4] = b10 * a01 + b11 * a11 + b12 * a21; + out[5] = b10 * a02 + b11 * a12 + b12 * a22; + + out[6] = b20 * a00 + b21 * a10 + b22 * a20; + out[7] = b20 * a01 + b21 * a11 + b22 * a21; + out[8] = b20 * a02 + b21 * a12 + b22 * a22; + return out; +}; + +/** + * Alias for {@link mat3.multiply} + * @function + */ +mat3.mul = mat3.multiply; + +/** + * Translate a mat3 by the given vector + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the matrix to translate + * @param {vec2} v vector to translate by + * @returns {mat3} out + */ +mat3.translate = function(out, a, v) { + var a00 = a[0], a01 = a[1], a02 = a[2], + a10 = a[3], a11 = a[4], a12 = a[5], + a20 = a[6], a21 = a[7], a22 = a[8], + x = v[0], y = v[1]; + + out[0] = a00; + out[1] = a01; + out[2] = a02; + + out[3] = a10; + out[4] = a11; + out[5] = a12; + + out[6] = x * a00 + y * a10 + a20; + out[7] = x * a01 + y * a11 + a21; + out[8] = x * a02 + y * a12 + a22; + return out; +}; + +/** + * Rotates a mat3 by the given angle + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat3} out + */ +mat3.rotate = function (out, a, rad) { + var a00 = a[0], a01 = a[1], a02 = a[2], + a10 = a[3], a11 = a[4], a12 = a[5], + a20 = a[6], a21 = a[7], a22 = a[8], + + s = Math.sin(rad), + c = Math.cos(rad); + + out[0] = c * a00 + s * a10; + out[1] = c * a01 + s * a11; + out[2] = c * a02 + s * a12; + + out[3] = c * a10 - s * a00; + out[4] = c * a11 - s * a01; + out[5] = c * a12 - s * a02; + + out[6] = a20; + out[7] = a21; + out[8] = a22; + return out; +}; + +/** + * Scales the mat3 by the dimensions in the given vec2 + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the matrix to rotate + * @param {vec2} v the vec2 to scale the matrix by + * @returns {mat3} out + **/ +mat3.scale = function(out, a, v) { + var x = v[0], y = v[1]; + + out[0] = x * a[0]; + out[1] = x * a[1]; + out[2] = x * a[2]; + + out[3] = y * a[3]; + out[4] = y * a[4]; + out[5] = y * a[5]; + + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + return out; +}; + +/** + * Copies the values from a mat2d into a mat3 + * + * @param {mat3} out the receiving matrix + * @param {mat2d} a the matrix to copy + * @returns {mat3} out + **/ +mat3.fromMat2d = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = 0; + + out[3] = a[2]; + out[4] = a[3]; + out[5] = 0; + + out[6] = a[4]; + out[7] = a[5]; + out[8] = 1; + return out; +}; + +/** +* Calculates a 3x3 matrix from the given quaternion +* +* @param {mat3} out mat3 receiving operation result +* @param {quat} q Quaternion to create matrix from +* +* @returns {mat3} out +*/ +mat3.fromQuat = function (out, q) { + var x = q[0], y = q[1], z = q[2], w = q[3], + x2 = x + x, + y2 = y + y, + z2 = z + z, + + xx = x * x2, + yx = y * x2, + yy = y * y2, + zx = z * x2, + zy = z * y2, + zz = z * z2, + wx = w * x2, + wy = w * y2, + wz = w * z2; + + out[0] = 1 - yy - zz; + out[3] = yx - wz; + out[6] = zx + wy; + + out[1] = yx + wz; + out[4] = 1 - xx - zz; + out[7] = zy - wx; + + out[2] = zx - wy; + out[5] = zy + wx; + out[8] = 1 - xx - yy; + + return out; +}; + +/** +* Calculates a 3x3 normal matrix (transpose inverse) from the 4x4 matrix +* +* @param {mat3} out mat3 receiving operation result +* @param {mat4} a Mat4 to derive the normal matrix from +* +* @returns {mat3} out +*/ +mat3.normalFromMat4 = function (out, a) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], + + b00 = a00 * a11 - a01 * a10, + b01 = a00 * a12 - a02 * a10, + b02 = a00 * a13 - a03 * a10, + b03 = a01 * a12 - a02 * a11, + b04 = a01 * a13 - a03 * a11, + b05 = a02 * a13 - a03 * a12, + b06 = a20 * a31 - a21 * a30, + b07 = a20 * a32 - a22 * a30, + b08 = a20 * a33 - a23 * a30, + b09 = a21 * a32 - a22 * a31, + b10 = a21 * a33 - a23 * a31, + b11 = a22 * a33 - a23 * a32, + + // Calculate the determinant + det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + + if (!det) { + return null; + } + det = 1.0 / det; + + out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; + out[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det; + out[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det; + + out[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det; + out[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det; + out[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det; + + out[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det; + out[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det; + out[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det; + + return out; +}; + +/** + * Returns a string representation of a mat3 + * + * @param {mat3} mat matrix to represent as a string + * @returns {String} string representation of the matrix + */ +mat3.str = function (a) { + return 'mat3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + + a[3] + ', ' + a[4] + ', ' + a[5] + ', ' + + a[6] + ', ' + a[7] + ', ' + a[8] + ')'; +}; + +/** + * Returns Frobenius norm of a mat3 + * + * @param {mat3} a the matrix to calculate Frobenius norm of + * @returns {Number} Frobenius norm + */ +mat3.frob = function (a) { + return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2))) +}; + + +if(typeof(exports) !== 'undefined') { + exports.mat3 = mat3; +} +; +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @class 4x4 Matrix + * @name mat4 + */ + +var mat4 = {}; + +/** + * Creates a new identity mat4 + * + * @returns {mat4} a new 4x4 matrix + */ +mat4.create = function() { + var out = new GLMAT_ARRAY_TYPE(16); + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = 1; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 1; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +}; + +/** + * Creates a new mat4 initialized with values from an existing matrix + * + * @param {mat4} a matrix to clone + * @returns {mat4} a new 4x4 matrix + */ +mat4.clone = function(a) { + var out = new GLMAT_ARRAY_TYPE(16); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + out[9] = a[9]; + out[10] = a[10]; + out[11] = a[11]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + return out; +}; + +/** + * Copy the values from one mat4 to another + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the source matrix + * @returns {mat4} out + */ +mat4.copy = function(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + out[9] = a[9]; + out[10] = a[10]; + out[11] = a[11]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + return out; +}; + +/** + * Set a mat4 to the identity matrix + * + * @param {mat4} out the receiving matrix + * @returns {mat4} out + */ +mat4.identity = function(out) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = 1; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 1; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +}; + +/** + * Transpose the values of a mat4 + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the source matrix + * @returns {mat4} out + */ +mat4.transpose = function(out, a) { + // If we are transposing ourselves we can skip a few steps but have to cache some values + if (out === a) { + var a01 = a[1], a02 = a[2], a03 = a[3], + a12 = a[6], a13 = a[7], + a23 = a[11]; + + out[1] = a[4]; + out[2] = a[8]; + out[3] = a[12]; + out[4] = a01; + out[6] = a[9]; + out[7] = a[13]; + out[8] = a02; + out[9] = a12; + out[11] = a[14]; + out[12] = a03; + out[13] = a13; + out[14] = a23; + } else { + out[0] = a[0]; + out[1] = a[4]; + out[2] = a[8]; + out[3] = a[12]; + out[4] = a[1]; + out[5] = a[5]; + out[6] = a[9]; + out[7] = a[13]; + out[8] = a[2]; + out[9] = a[6]; + out[10] = a[10]; + out[11] = a[14]; + out[12] = a[3]; + out[13] = a[7]; + out[14] = a[11]; + out[15] = a[15]; + } + + return out; +}; + +/** + * Inverts a mat4 + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the source matrix + * @returns {mat4} out + */ +mat4.invert = function(out, a) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], + + b00 = a00 * a11 - a01 * a10, + b01 = a00 * a12 - a02 * a10, + b02 = a00 * a13 - a03 * a10, + b03 = a01 * a12 - a02 * a11, + b04 = a01 * a13 - a03 * a11, + b05 = a02 * a13 - a03 * a12, + b06 = a20 * a31 - a21 * a30, + b07 = a20 * a32 - a22 * a30, + b08 = a20 * a33 - a23 * a30, + b09 = a21 * a32 - a22 * a31, + b10 = a21 * a33 - a23 * a31, + b11 = a22 * a33 - a23 * a32, + + // Calculate the determinant + det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + + if (!det) { + return null; + } + det = 1.0 / det; + + out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; + out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; + out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; + out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; + out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; + out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; + out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; + out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; + out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; + out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; + out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; + out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; + out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; + out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; + out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; + out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; + + return out; +}; + +/** + * Calculates the adjugate of a mat4 + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the source matrix + * @returns {mat4} out + */ +mat4.adjoint = function(out, a) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; + + out[0] = (a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22)); + out[1] = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22)); + out[2] = (a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12)); + out[3] = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12)); + out[4] = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22)); + out[5] = (a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22)); + out[6] = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12)); + out[7] = (a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12)); + out[8] = (a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21)); + out[9] = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21)); + out[10] = (a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11)); + out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11)); + out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21)); + out[13] = (a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21)); + out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11)); + out[15] = (a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11)); + return out; +}; + +/** + * Calculates the determinant of a mat4 + * + * @param {mat4} a the source matrix + * @returns {Number} determinant of a + */ +mat4.determinant = function (a) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], + + b00 = a00 * a11 - a01 * a10, + b01 = a00 * a12 - a02 * a10, + b02 = a00 * a13 - a03 * a10, + b03 = a01 * a12 - a02 * a11, + b04 = a01 * a13 - a03 * a11, + b05 = a02 * a13 - a03 * a12, + b06 = a20 * a31 - a21 * a30, + b07 = a20 * a32 - a22 * a30, + b08 = a20 * a33 - a23 * a30, + b09 = a21 * a32 - a22 * a31, + b10 = a21 * a33 - a23 * a31, + b11 = a22 * a33 - a23 * a32; + + // Calculate the determinant + return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; +}; + +/** + * Multiplies two mat4's + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the first operand + * @param {mat4} b the second operand + * @returns {mat4} out + */ +mat4.multiply = function (out, a, b) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; + + // Cache only the current line of the second matrix + var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; + out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; + out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; + out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; + out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + return out; +}; + +/** + * Alias for {@link mat4.multiply} + * @function + */ +mat4.mul = mat4.multiply; + +/** + * Translate a mat4 by the given vector + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to translate + * @param {vec3} v vector to translate by + * @returns {mat4} out + */ +mat4.translate = function (out, a, v) { + var x = v[0], y = v[1], z = v[2], + a00, a01, a02, a03, + a10, a11, a12, a13, + a20, a21, a22, a23; + + if (a === out) { + out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; + out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; + out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; + out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; + } else { + a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; + a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; + a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; + + out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03; + out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13; + out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23; + + out[12] = a00 * x + a10 * y + a20 * z + a[12]; + out[13] = a01 * x + a11 * y + a21 * z + a[13]; + out[14] = a02 * x + a12 * y + a22 * z + a[14]; + out[15] = a03 * x + a13 * y + a23 * z + a[15]; + } + + return out; +}; + +/** + * Scales the mat4 by the dimensions in the given vec3 + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to scale + * @param {vec3} v the vec3 to scale the matrix by + * @returns {mat4} out + **/ +mat4.scale = function(out, a, v) { + var x = v[0], y = v[1], z = v[2]; + + out[0] = a[0] * x; + out[1] = a[1] * x; + out[2] = a[2] * x; + out[3] = a[3] * x; + out[4] = a[4] * y; + out[5] = a[5] * y; + out[6] = a[6] * y; + out[7] = a[7] * y; + out[8] = a[8] * z; + out[9] = a[9] * z; + out[10] = a[10] * z; + out[11] = a[11] * z; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + return out; +}; + +/** + * Rotates a mat4 by the given angle + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @param {vec3} axis the axis to rotate around + * @returns {mat4} out + */ +mat4.rotate = function (out, a, rad, axis) { + var x = axis[0], y = axis[1], z = axis[2], + len = Math.sqrt(x * x + y * y + z * z), + s, c, t, + a00, a01, a02, a03, + a10, a11, a12, a13, + a20, a21, a22, a23, + b00, b01, b02, + b10, b11, b12, + b20, b21, b22; + + if (Math.abs(len) < GLMAT_EPSILON) { return null; } + + len = 1 / len; + x *= len; + y *= len; + z *= len; + + s = Math.sin(rad); + c = Math.cos(rad); + t = 1 - c; + + a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; + a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; + a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; + + // Construct the elements of the rotation matrix + b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s; + b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s; + b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c; + + // Perform rotation-specific matrix multiplication + out[0] = a00 * b00 + a10 * b01 + a20 * b02; + out[1] = a01 * b00 + a11 * b01 + a21 * b02; + out[2] = a02 * b00 + a12 * b01 + a22 * b02; + out[3] = a03 * b00 + a13 * b01 + a23 * b02; + out[4] = a00 * b10 + a10 * b11 + a20 * b12; + out[5] = a01 * b10 + a11 * b11 + a21 * b12; + out[6] = a02 * b10 + a12 * b11 + a22 * b12; + out[7] = a03 * b10 + a13 * b11 + a23 * b12; + out[8] = a00 * b20 + a10 * b21 + a20 * b22; + out[9] = a01 * b20 + a11 * b21 + a21 * b22; + out[10] = a02 * b20 + a12 * b21 + a22 * b22; + out[11] = a03 * b20 + a13 * b21 + a23 * b22; + + if (a !== out) { // If the source and destination differ, copy the unchanged last row + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + } + return out; +}; + +/** + * Rotates a matrix by the given angle around the X axis + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat4} out + */ +mat4.rotateX = function (out, a, rad) { + var s = Math.sin(rad), + c = Math.cos(rad), + a10 = a[4], + a11 = a[5], + a12 = a[6], + a13 = a[7], + a20 = a[8], + a21 = a[9], + a22 = a[10], + a23 = a[11]; + + if (a !== out) { // If the source and destination differ, copy the unchanged rows + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + } + + // Perform axis-specific matrix multiplication + out[4] = a10 * c + a20 * s; + out[5] = a11 * c + a21 * s; + out[6] = a12 * c + a22 * s; + out[7] = a13 * c + a23 * s; + out[8] = a20 * c - a10 * s; + out[9] = a21 * c - a11 * s; + out[10] = a22 * c - a12 * s; + out[11] = a23 * c - a13 * s; + return out; +}; + +/** + * Rotates a matrix by the given angle around the Y axis + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat4} out + */ +mat4.rotateY = function (out, a, rad) { + var s = Math.sin(rad), + c = Math.cos(rad), + a00 = a[0], + a01 = a[1], + a02 = a[2], + a03 = a[3], + a20 = a[8], + a21 = a[9], + a22 = a[10], + a23 = a[11]; + + if (a !== out) { // If the source and destination differ, copy the unchanged rows + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + } + + // Perform axis-specific matrix multiplication + out[0] = a00 * c - a20 * s; + out[1] = a01 * c - a21 * s; + out[2] = a02 * c - a22 * s; + out[3] = a03 * c - a23 * s; + out[8] = a00 * s + a20 * c; + out[9] = a01 * s + a21 * c; + out[10] = a02 * s + a22 * c; + out[11] = a03 * s + a23 * c; + return out; +}; + +/** + * Rotates a matrix by the given angle around the Z axis + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat4} out + */ +mat4.rotateZ = function (out, a, rad) { + var s = Math.sin(rad), + c = Math.cos(rad), + a00 = a[0], + a01 = a[1], + a02 = a[2], + a03 = a[3], + a10 = a[4], + a11 = a[5], + a12 = a[6], + a13 = a[7]; + + if (a !== out) { // If the source and destination differ, copy the unchanged last row + out[8] = a[8]; + out[9] = a[9]; + out[10] = a[10]; + out[11] = a[11]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + } + + // Perform axis-specific matrix multiplication + out[0] = a00 * c + a10 * s; + out[1] = a01 * c + a11 * s; + out[2] = a02 * c + a12 * s; + out[3] = a03 * c + a13 * s; + out[4] = a10 * c - a00 * s; + out[5] = a11 * c - a01 * s; + out[6] = a12 * c - a02 * s; + out[7] = a13 * c - a03 * s; + return out; +}; + +/** + * Creates a matrix from a quaternion rotation and vector translation + * This is equivalent to (but much faster than): + * + * mat4.identity(dest); + * mat4.translate(dest, vec); + * var quatMat = mat4.create(); + * quat4.toMat4(quat, quatMat); + * mat4.multiply(dest, quatMat); + * + * @param {mat4} out mat4 receiving operation result + * @param {quat4} q Rotation quaternion + * @param {vec3} v Translation vector + * @returns {mat4} out + */ +mat4.fromRotationTranslation = function (out, q, v) { + // Quaternion math + var x = q[0], y = q[1], z = q[2], w = q[3], + x2 = x + x, + y2 = y + y, + z2 = z + z, + + xx = x * x2, + xy = x * y2, + xz = x * z2, + yy = y * y2, + yz = y * z2, + zz = z * z2, + wx = w * x2, + wy = w * y2, + wz = w * z2; + + out[0] = 1 - (yy + zz); + out[1] = xy + wz; + out[2] = xz - wy; + out[3] = 0; + out[4] = xy - wz; + out[5] = 1 - (xx + zz); + out[6] = yz + wx; + out[7] = 0; + out[8] = xz + wy; + out[9] = yz - wx; + out[10] = 1 - (xx + yy); + out[11] = 0; + out[12] = v[0]; + out[13] = v[1]; + out[14] = v[2]; + out[15] = 1; + + return out; +}; + +mat4.fromQuat = function (out, q) { + var x = q[0], y = q[1], z = q[2], w = q[3], + x2 = x + x, + y2 = y + y, + z2 = z + z, + + xx = x * x2, + yx = y * x2, + yy = y * y2, + zx = z * x2, + zy = z * y2, + zz = z * z2, + wx = w * x2, + wy = w * y2, + wz = w * z2; + + out[0] = 1 - yy - zz; + out[1] = yx + wz; + out[2] = zx - wy; + out[3] = 0; + + out[4] = yx - wz; + out[5] = 1 - xx - zz; + out[6] = zy + wx; + out[7] = 0; + + out[8] = zx + wy; + out[9] = zy - wx; + out[10] = 1 - xx - yy; + out[11] = 0; + + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + + return out; +}; + +/** + * Generates a frustum matrix with the given bounds + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {Number} left Left bound of the frustum + * @param {Number} right Right bound of the frustum + * @param {Number} bottom Bottom bound of the frustum + * @param {Number} top Top bound of the frustum + * @param {Number} near Near bound of the frustum + * @param {Number} far Far bound of the frustum + * @returns {mat4} out + */ +mat4.frustum = function (out, left, right, bottom, top, near, far) { + var rl = 1 / (right - left), + tb = 1 / (top - bottom), + nf = 1 / (near - far); + out[0] = (near * 2) * rl; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = (near * 2) * tb; + out[6] = 0; + out[7] = 0; + out[8] = (right + left) * rl; + out[9] = (top + bottom) * tb; + out[10] = (far + near) * nf; + out[11] = -1; + out[12] = 0; + out[13] = 0; + out[14] = (far * near * 2) * nf; + out[15] = 0; + return out; +}; + +/** + * Generates a perspective projection matrix with the given bounds + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {number} fovy Vertical field of view in radians + * @param {number} aspect Aspect ratio. typically viewport width/height + * @param {number} near Near bound of the frustum + * @param {number} far Far bound of the frustum + * @returns {mat4} out + */ +mat4.perspective = function (out, fovy, aspect, near, far) { + var f = 1.0 / Math.tan(fovy / 2), + nf = 1 / (near - far); + out[0] = f / aspect; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = f; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = (far + near) * nf; + out[11] = -1; + out[12] = 0; + out[13] = 0; + out[14] = (2 * far * near) * nf; + out[15] = 0; + return out; +}; + +/** + * Generates a orthogonal projection matrix with the given bounds + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {number} left Left bound of the frustum + * @param {number} right Right bound of the frustum + * @param {number} bottom Bottom bound of the frustum + * @param {number} top Top bound of the frustum + * @param {number} near Near bound of the frustum + * @param {number} far Far bound of the frustum + * @returns {mat4} out + */ +mat4.ortho = function (out, left, right, bottom, top, near, far) { + var lr = 1 / (left - right), + bt = 1 / (bottom - top), + nf = 1 / (near - far); + out[0] = -2 * lr; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = -2 * bt; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 2 * nf; + out[11] = 0; + out[12] = (left + right) * lr; + out[13] = (top + bottom) * bt; + out[14] = (far + near) * nf; + out[15] = 1; + return out; +}; + +/** + * Generates a look-at matrix with the given eye position, focal point, and up axis + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {vec3} eye Position of the viewer + * @param {vec3} center Point the viewer is looking at + * @param {vec3} up vec3 pointing up + * @returns {mat4} out + */ +mat4.lookAt = function (out, eye, center, up) { + var x0, x1, x2, y0, y1, y2, z0, z1, z2, len, + eyex = eye[0], + eyey = eye[1], + eyez = eye[2], + upx = up[0], + upy = up[1], + upz = up[2], + centerx = center[0], + centery = center[1], + centerz = center[2]; + + if (Math.abs(eyex - centerx) < GLMAT_EPSILON && + Math.abs(eyey - centery) < GLMAT_EPSILON && + Math.abs(eyez - centerz) < GLMAT_EPSILON) { + return mat4.identity(out); + } + + z0 = eyex - centerx; + z1 = eyey - centery; + z2 = eyez - centerz; + + len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); + z0 *= len; + z1 *= len; + z2 *= len; + + x0 = upy * z2 - upz * z1; + x1 = upz * z0 - upx * z2; + x2 = upx * z1 - upy * z0; + len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); + if (!len) { + x0 = 0; + x1 = 0; + x2 = 0; + } else { + len = 1 / len; + x0 *= len; + x1 *= len; + x2 *= len; + } + + y0 = z1 * x2 - z2 * x1; + y1 = z2 * x0 - z0 * x2; + y2 = z0 * x1 - z1 * x0; + + len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); + if (!len) { + y0 = 0; + y1 = 0; + y2 = 0; + } else { + len = 1 / len; + y0 *= len; + y1 *= len; + y2 *= len; + } + + out[0] = x0; + out[1] = y0; + out[2] = z0; + out[3] = 0; + out[4] = x1; + out[5] = y1; + out[6] = z1; + out[7] = 0; + out[8] = x2; + out[9] = y2; + out[10] = z2; + out[11] = 0; + out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); + out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); + out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); + out[15] = 1; + + return out; +}; + +/** + * Returns a string representation of a mat4 + * + * @param {mat4} mat matrix to represent as a string + * @returns {String} string representation of the matrix + */ +mat4.str = function (a) { + return 'mat4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ', ' + + a[4] + ', ' + a[5] + ', ' + a[6] + ', ' + a[7] + ', ' + + a[8] + ', ' + a[9] + ', ' + a[10] + ', ' + a[11] + ', ' + + a[12] + ', ' + a[13] + ', ' + a[14] + ', ' + a[15] + ')'; +}; + +/** + * Returns Frobenius norm of a mat4 + * + * @param {mat4} a the matrix to calculate Frobenius norm of + * @returns {Number} Frobenius norm + */ +mat4.frob = function (a) { + return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2) + Math.pow(a[9], 2) + Math.pow(a[10], 2) + Math.pow(a[11], 2) + Math.pow(a[12], 2) + Math.pow(a[13], 2) + Math.pow(a[14], 2) + Math.pow(a[15], 2) )) +}; + + +if(typeof(exports) !== 'undefined') { + exports.mat4 = mat4; +} +; +/* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @class Quaternion + * @name quat + */ + +var quat = {}; + +/** + * Creates a new identity quat + * + * @returns {quat} a new quaternion + */ +quat.create = function() { + var out = new GLMAT_ARRAY_TYPE(4); + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; +}; + +/** + * Sets a quaternion to represent the shortest rotation from one + * vector to another. + * + * Both vectors are assumed to be unit length. + * + * @param {quat} out the receiving quaternion. + * @param {vec3} a the initial vector + * @param {vec3} b the destination vector + * @returns {quat} out + */ +quat.rotationTo = (function() { + var tmpvec3 = vec3.create(); + var xUnitVec3 = vec3.fromValues(1,0,0); + var yUnitVec3 = vec3.fromValues(0,1,0); + + return function(out, a, b) { + var dot = vec3.dot(a, b); + if (dot < -0.999999) { + vec3.cross(tmpvec3, xUnitVec3, a); + if (vec3.length(tmpvec3) < 0.000001) + vec3.cross(tmpvec3, yUnitVec3, a); + vec3.normalize(tmpvec3, tmpvec3); + quat.setAxisAngle(out, tmpvec3, Math.PI); + return out; + } else if (dot > 0.999999) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; + } else { + vec3.cross(tmpvec3, a, b); + out[0] = tmpvec3[0]; + out[1] = tmpvec3[1]; + out[2] = tmpvec3[2]; + out[3] = 1 + dot; + return quat.normalize(out, out); + } + }; +})(); + +/** + * Sets the specified quaternion with values corresponding to the given + * axes. Each axis is a vec3 and is expected to be unit length and + * perpendicular to all other specified axes. + * + * @param {vec3} view the vector representing the viewing direction + * @param {vec3} right the vector representing the local "right" direction + * @param {vec3} up the vector representing the local "up" direction + * @returns {quat} out + */ +quat.setAxes = (function() { + var matr = mat3.create(); + + return function(out, view, right, up) { + matr[0] = right[0]; + matr[3] = right[1]; + matr[6] = right[2]; + + matr[1] = up[0]; + matr[4] = up[1]; + matr[7] = up[2]; + + matr[2] = -view[0]; + matr[5] = -view[1]; + matr[8] = -view[2]; + + return quat.normalize(out, quat.fromMat3(out, matr)); + }; +})(); + +/** + * Creates a new quat initialized with values from an existing quaternion + * + * @param {quat} a quaternion to clone + * @returns {quat} a new quaternion + * @function + */ +quat.clone = vec4.clone; + +/** + * Creates a new quat initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {quat} a new quaternion + * @function + */ +quat.fromValues = vec4.fromValues; + +/** + * Copy the values from one quat to another + * + * @param {quat} out the receiving quaternion + * @param {quat} a the source quaternion + * @returns {quat} out + * @function + */ +quat.copy = vec4.copy; + +/** + * Set the components of a quat to the given values + * + * @param {quat} out the receiving quaternion + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {quat} out + * @function + */ +quat.set = vec4.set; + +/** + * Set a quat to the identity quaternion + * + * @param {quat} out the receiving quaternion + * @returns {quat} out + */ +quat.identity = function(out) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; +}; + +/** + * Sets a quat from the given angle and rotation axis, + * then returns it. + * + * @param {quat} out the receiving quaternion + * @param {vec3} axis the axis around which to rotate + * @param {Number} rad the angle in radians + * @returns {quat} out + **/ +quat.setAxisAngle = function(out, axis, rad) { + rad = rad * 0.5; + var s = Math.sin(rad); + out[0] = s * axis[0]; + out[1] = s * axis[1]; + out[2] = s * axis[2]; + out[3] = Math.cos(rad); + return out; +}; + +/** + * Adds two quat's + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @returns {quat} out + * @function + */ +quat.add = vec4.add; + +/** + * Multiplies two quat's + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @returns {quat} out + */ +quat.multiply = function(out, a, b) { + var ax = a[0], ay = a[1], az = a[2], aw = a[3], + bx = b[0], by = b[1], bz = b[2], bw = b[3]; + + out[0] = ax * bw + aw * bx + ay * bz - az * by; + out[1] = ay * bw + aw * by + az * bx - ax * bz; + out[2] = az * bw + aw * bz + ax * by - ay * bx; + out[3] = aw * bw - ax * bx - ay * by - az * bz; + return out; +}; + +/** + * Alias for {@link quat.multiply} + * @function + */ +quat.mul = quat.multiply; + +/** + * Scales a quat by a scalar number + * + * @param {quat} out the receiving vector + * @param {quat} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {quat} out + * @function + */ +quat.scale = vec4.scale; + +/** + * Rotates a quaternion by the given angle about the X axis + * + * @param {quat} out quat receiving operation result + * @param {quat} a quat to rotate + * @param {number} rad angle (in radians) to rotate + * @returns {quat} out + */ +quat.rotateX = function (out, a, rad) { + rad *= 0.5; + + var ax = a[0], ay = a[1], az = a[2], aw = a[3], + bx = Math.sin(rad), bw = Math.cos(rad); + + out[0] = ax * bw + aw * bx; + out[1] = ay * bw + az * bx; + out[2] = az * bw - ay * bx; + out[3] = aw * bw - ax * bx; + return out; +}; + +/** + * Rotates a quaternion by the given angle about the Y axis + * + * @param {quat} out quat receiving operation result + * @param {quat} a quat to rotate + * @param {number} rad angle (in radians) to rotate + * @returns {quat} out + */ +quat.rotateY = function (out, a, rad) { + rad *= 0.5; + + var ax = a[0], ay = a[1], az = a[2], aw = a[3], + by = Math.sin(rad), bw = Math.cos(rad); + + out[0] = ax * bw - az * by; + out[1] = ay * bw + aw * by; + out[2] = az * bw + ax * by; + out[3] = aw * bw - ay * by; + return out; +}; + +/** + * Rotates a quaternion by the given angle about the Z axis + * + * @param {quat} out quat receiving operation result + * @param {quat} a quat to rotate + * @param {number} rad angle (in radians) to rotate + * @returns {quat} out + */ +quat.rotateZ = function (out, a, rad) { + rad *= 0.5; + + var ax = a[0], ay = a[1], az = a[2], aw = a[3], + bz = Math.sin(rad), bw = Math.cos(rad); + + out[0] = ax * bw + ay * bz; + out[1] = ay * bw - ax * bz; + out[2] = az * bw + aw * bz; + out[3] = aw * bw - az * bz; + return out; +}; + +/** + * Calculates the W component of a quat from the X, Y, and Z components. + * Assumes that quaternion is 1 unit in length. + * Any existing W component will be ignored. + * + * @param {quat} out the receiving quaternion + * @param {quat} a quat to calculate W component of + * @returns {quat} out + */ +quat.calculateW = function (out, a) { + var x = a[0], y = a[1], z = a[2]; + + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = -Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z)); + return out; +}; + +/** + * Calculates the dot product of two quat's + * + * @param {quat} a the first operand + * @param {quat} b the second operand + * @returns {Number} dot product of a and b + * @function + */ +quat.dot = vec4.dot; + +/** + * Performs a linear interpolation between two quat's + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @param {Number} t interpolation amount between the two inputs + * @returns {quat} out + * @function + */ +quat.lerp = vec4.lerp; + +/** + * Performs a spherical linear interpolation between two quat + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @param {Number} t interpolation amount between the two inputs + * @returns {quat} out + */ +quat.slerp = function (out, a, b, t) { + // benchmarks: + // http://jsperf.com/quaternion-slerp-implementations + + var ax = a[0], ay = a[1], az = a[2], aw = a[3], + bx = b[0], by = b[1], bz = b[2], bw = b[3]; + + var omega, cosom, sinom, scale0, scale1; + + // calc cosine + cosom = ax * bx + ay * by + az * bz + aw * bw; + // adjust signs (if necessary) + if ( cosom < 0.0 ) { + cosom = -cosom; + bx = - bx; + by = - by; + bz = - bz; + bw = - bw; + } + // calculate coefficients + if ( (1.0 - cosom) > 0.000001 ) { + // standard case (slerp) + omega = Math.acos(cosom); + sinom = Math.sin(omega); + scale0 = Math.sin((1.0 - t) * omega) / sinom; + scale1 = Math.sin(t * omega) / sinom; + } else { + // "from" and "to" quaternions are very close + // ... so we can do a linear interpolation + scale0 = 1.0 - t; + scale1 = t; + } + // calculate final values + out[0] = scale0 * ax + scale1 * bx; + out[1] = scale0 * ay + scale1 * by; + out[2] = scale0 * az + scale1 * bz; + out[3] = scale0 * aw + scale1 * bw; + + return out; +}; + +/** + * Calculates the inverse of a quat + * + * @param {quat} out the receiving quaternion + * @param {quat} a quat to calculate inverse of + * @returns {quat} out + */ +quat.invert = function(out, a) { + var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], + dot = a0*a0 + a1*a1 + a2*a2 + a3*a3, + invDot = dot ? 1.0/dot : 0; + + // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0 + + out[0] = -a0*invDot; + out[1] = -a1*invDot; + out[2] = -a2*invDot; + out[3] = a3*invDot; + return out; +}; + +/** + * Calculates the conjugate of a quat + * If the quaternion is normalized, this function is faster than quat.inverse and produces the same result. + * + * @param {quat} out the receiving quaternion + * @param {quat} a quat to calculate conjugate of + * @returns {quat} out + */ +quat.conjugate = function (out, a) { + out[0] = -a[0]; + out[1] = -a[1]; + out[2] = -a[2]; + out[3] = a[3]; + return out; +}; + +/** + * Calculates the length of a quat + * + * @param {quat} a vector to calculate length of + * @returns {Number} length of a + * @function + */ +quat.length = vec4.length; + +/** + * Alias for {@link quat.length} + * @function + */ +quat.len = quat.length; + +/** + * Calculates the squared length of a quat + * + * @param {quat} a vector to calculate squared length of + * @returns {Number} squared length of a + * @function + */ +quat.squaredLength = vec4.squaredLength; + +/** + * Alias for {@link quat.squaredLength} + * @function + */ +quat.sqrLen = quat.squaredLength; + +/** + * Normalize a quat + * + * @param {quat} out the receiving quaternion + * @param {quat} a quaternion to normalize + * @returns {quat} out + * @function + */ +quat.normalize = vec4.normalize; + +/** + * Creates a quaternion from the given 3x3 rotation matrix. + * + * NOTE: The resultant quaternion is not normalized, so you should be sure + * to renormalize the quaternion yourself where necessary. + * + * @param {quat} out the receiving quaternion + * @param {mat3} m rotation matrix + * @returns {quat} out + * @function + */ +quat.fromMat3 = function(out, m) { + // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes + // article "Quaternion Calculus and Fast Animation". + var fTrace = m[0] + m[4] + m[8]; + var fRoot; + + if ( fTrace > 0.0 ) { + // |w| > 1/2, may as well choose w > 1/2 + fRoot = Math.sqrt(fTrace + 1.0); // 2w + out[3] = 0.5 * fRoot; + fRoot = 0.5/fRoot; // 1/(4w) + out[0] = (m[7]-m[5])*fRoot; + out[1] = (m[2]-m[6])*fRoot; + out[2] = (m[3]-m[1])*fRoot; + } else { + // |w| <= 1/2 + var i = 0; + if ( m[4] > m[0] ) + i = 1; + if ( m[8] > m[i*3+i] ) + i = 2; + var j = (i+1)%3; + var k = (i+2)%3; + + fRoot = Math.sqrt(m[i*3+i]-m[j*3+j]-m[k*3+k] + 1.0); + out[i] = 0.5 * fRoot; + fRoot = 0.5 / fRoot; + out[3] = (m[k*3+j] - m[j*3+k]) * fRoot; + out[j] = (m[j*3+i] + m[i*3+j]) * fRoot; + out[k] = (m[k*3+i] + m[i*3+k]) * fRoot; + } + + return out; +}; + +/** + * Returns a string representation of a quatenion + * + * @param {quat} vec vector to represent as a string + * @returns {String} string representation of the vector + */ +quat.str = function (a) { + return 'quat(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')'; +}; + +if(typeof(exports) !== 'undefined') { + exports.quat = quat; +} +; + + + + + + + + + + + + + + })(shim.exports); +})(this); + +},{}],24:[function(require,module,exports){ +// Underscore.js 1.4.4 +// http://underscorejs.org +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.4.4'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? null : []; + return _[first ? 'find' : 'filter'](obj, function(value) { + for (var key in attrs) { + if (attrs[key] !== value[key]) return false; + } + return true; + }); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See: https://bugs.webkit.org/show_bug.cgi?id=80797 + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity, value: -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity, value: Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + return _.isFunction(value) ? value : function(obj){ return obj[value]; }; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, value, context) { + var iterator = lookupIterator(value); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + index : index, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index < right.index ? -1 : 1; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(obj, value, context, behavior) { + var result = {}; + var iterator = lookupIterator(value || _.identity); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, value, context) { + return group(obj, value, context, function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(value); + }); + }; + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = function(obj, value, context) { + return group(obj, value, context, function(result, key) { + if (!_.has(result, key)) result[key] = 0; + result[key]++; + }); + }; + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = iterator == null ? _.identity : lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + each(input, function(value) { + if (_.isArray(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(concat.apply(ArrayProto, arguments)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(args, "" + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, l = list.length; i < l; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, l = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < l; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + var args = slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(slice.call(arguments))); + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, result; + var previous = 0; + var later = function() { + previous = new Date; + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) result = func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(context, args); + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func]; + push.apply(args, arguments); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var values = []; + for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var pairs = []; + for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(n); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named property is a function then invoke it; + // otherwise, return it. + _.result = function(object, property) { + if (object == null) return null; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + +}).call(this); + +},{}],25:[function(require,module,exports){ +if (typeof(window) !== 'undefined' && typeof(window.requestAnimationFrame) !== 'function') { + window.requestAnimationFrame = ( + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { setTimeout(callback, 1000 / 60); } + ); +} + +Leap = require("../lib/index"); + +},{"../lib/index":11}]},{},[25]) +; \ No newline at end of file