From f9a613209d72abc13475c9f4958f35d7162dc303 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Tue, 2 Jul 2019 10:39:23 +0200 Subject: [PATCH] feat: encoding/decoding works for float and negative feat: implemented tests for encoding/decoding --- ...tlasViewer.constantService.service.spec.ts | 110 ++++++++++++++++++ .../atlasViewer.constantService.service.ts | 65 +++++++++-- .../atlasViewer.urlService.service.ts | 51 ++++++-- 3 files changed, 206 insertions(+), 20 deletions(-) create mode 100644 src/atlasViewer/atlasViewer.constantService.service.spec.ts diff --git a/src/atlasViewer/atlasViewer.constantService.service.spec.ts b/src/atlasViewer/atlasViewer.constantService.service.spec.ts new file mode 100644 index 000000000..71c9ecd02 --- /dev/null +++ b/src/atlasViewer/atlasViewer.constantService.service.spec.ts @@ -0,0 +1,110 @@ +import { encodeNumber, decodeToNumber } from './atlasViewer.constantService.service' +import {} from 'jasmine' + +const FLOAT_PRECISION = 6 + +describe('encodeNumber/decodeToNumber', () => { + + + const getCompareOriginal = (original: number[]) => (element:string, index: number) => + original[index].toString().length >= element.length + + + const lengthShortened = (original: number[], encodedString: string[]) => + encodedString.every(getCompareOriginal(original)) + + it('should encode/decode positive integer as expected', () => { + + const positiveInt = [ + 0, + 1, + 99999999999, + 12347 + ] + + const encodedString = positiveInt.map(n => encodeNumber(n)) + const decodedString = encodedString.map(s => decodeToNumber(s)) + expect(decodedString).toEqual(positiveInt) + + expect(lengthShortened(positiveInt, encodedString)).toBe(true) + }) + + it('should encode/decode ANY positive integer as expected', () => { + const posInt = Array(1000).fill(null).map(() => { + const numDig = Math.ceil(Math.random() * 7) + return Math.floor(Math.random() * Math.pow(10, numDig)) + }) + const encodedString = posInt.map(n => encodeNumber(n)) + const decodedNumber = encodedString.map(s => decodeToNumber(s)) + expect(decodedNumber).toEqual(posInt) + + expect(lengthShortened(posInt, encodedString)).toBe(true) + }) + + + it('should encode/decode signed integer as expected', () => { + + const signedInt = [ + 0, + -0, + -1, + 1, + 128, + -54 + ] + + const encodedString = signedInt.map(n => encodeNumber(n)) + const decodedNumber = encodedString.map(s => decodeToNumber(s)) + + /** + * -0 will be converted to 0 by the encode/decode process, but does not deep equal, according to jasmine + */ + expect(decodedNumber).toEqual(signedInt.map(v => v === 0 ? 0 : v)) + + expect(lengthShortened(signedInt, encodedString)).toBe(true) + }) + + it('should encode/decode ANY signed integer as expected', () => { + + const signedInt = Array(1000).fill(null).map(() => { + const numDig = Math.ceil(Math.random() * 7) + return Math.floor(Math.random() * Math.pow(10, numDig)) * (Math.random() > 0.5 ? 1 : -1) + }) + const encodedString = signedInt.map(n => encodeNumber(n)) + const decodedNumber = encodedString.map(s => decodeToNumber(s)) + + /** + * -0 will be converted to 0 by the encode/decode process, but does not deep equal, according to jasmine + */ + expect(decodedNumber).toEqual(signedInt.map(v => v === 0 ? 0 : v)) + + expect(lengthShortened(signedInt, encodedString)).toBe(true) + }) + + + it('should encode/decode float as expected', () => { + const floatNum = [ + 0.111, + 12.23, + 1723.0 + ] + + const encodedString = floatNum.map(f => encodeNumber(f, { float: true })) + const decodedNumber = encodedString.map(s => decodeToNumber(s, { float: true })) + expect(decodedNumber.map(n => n.toFixed(FLOAT_PRECISION))).toEqual(floatNum.map(n => n.toFixed(FLOAT_PRECISION))) + }) + + it('should encode/decode ANY float as expected', () => { + const floatNums = Array(1000).fill(null).map(() => { + const numDig = Math.ceil(Math.random() * 7) + return (Math.random() > 0.5 ? 1 : -1) * Math.floor( + Math.random() * Math.pow(10, numDig) + ) + }) + + const encodedString = floatNums.map(f => encodeNumber(f, { float: true })) + const decodedNumber = encodedString.map(s => decodeToNumber(s, { float: true })) + + expect(floatNums.map(v => v.toFixed(FLOAT_PRECISION))).toEqual(decodedNumber.map(n => n.toFixed(FLOAT_PRECISION))) + }) +}) \ No newline at end of file diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts index 22511be58..5bdeb5d62 100644 --- a/src/atlasViewer/atlasViewer.constantService.service.ts +++ b/src/atlasViewer/atlasViewer.constantService.service.ts @@ -307,20 +307,26 @@ export const SUPPORT_LIBRARY_MAP : Map<string,HTMLElement> = new Map([ * So performance is not really that important (Also, need to learn bitwise operation) */ -const cipher = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-" +const cipher = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-' export const separator = "." +const negFlag = '!' -export const encodeNumber = (number: number) => { - - if (isNaN(Number(number)) || number === null || - number === Number.POSITIVE_INFINITY) - throw "The input is not valid" - if (number < 0) - throw "Can't represent negative numbers now" +const encodeInt = (number: number) => { + if (number % 1 !== 0) throw 'cannot encodeInt on a float. Ensure float flag is set' + if (isNaN(Number(number)) || number === null || number === Number.POSITIVE_INFINITY) + throw 'The input is not valid' let rixit // like 'digit', only in some non-decimal radix - let residual = Math.floor(number) + let residual let result = '' + + if (number < 0) { + result += negFlag + residual = Math.floor(number * -1) + } else { + residual = Math.floor(number) + } + while (true) { rixit = residual % 64 // console.log("rixit : " + rixit) @@ -337,8 +343,45 @@ export const encodeNumber = (number: number) => { return result } -export const decodeToNumber = (encodedString: string) => { - return [...encodedString].reduce((acc,curr) => { +interface B64EncodingOption { + float: boolean +} + +const defaultB64EncodingOption = { + float: false +} + +export const encodeNumber: (number:number, option?: B64EncodingOption) => string = (number: number, { float = false }: B64EncodingOption = 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, negFlag = false + if (encodedString.slice(-1) === '!') { + negFlag = true + _encodedString = encodedString.slice(0, -1) + } else { + _encodedString = encodedString + } + return (negFlag ? -1 : 1) * [..._encodedString].reduce((acc,curr) => { return acc * 64 + cipher.indexOf(curr) }, 0) +} + +export const decodeToNumber: (encodedString:string, option?: B64EncodingOption) => 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] + } } \ No newline at end of file diff --git a/src/atlasViewer/atlasViewer.urlService.service.ts b/src/atlasViewer/atlasViewer.urlService.service.ts index db47cabb7..8506cf25f 100644 --- a/src/atlasViewer/atlasViewer.urlService.service.ts +++ b/src/atlasViewer/atlasViewer.urlService.service.ts @@ -148,6 +148,9 @@ export class AtlasViewerURLService{ /** * either or both parcellationToLoad and .regions maybe empty */ + /** + * backwards compatibility + */ const selectedRegionsParam = searchparams.get('regionsSelected') if(selectedRegionsParam){ const ids = selectedRegionsParam.split('_') @@ -167,7 +170,7 @@ export class AtlasViewerURLService{ for (let ngId in json) { const val = json[ngId] - const labelIndicies = val.split(separator).map(decodeToNumber) + const labelIndicies = val.split(separator).map(n =>decodeToNumber(n)) for (let labelIndex of labelIndicies) { selectRegionIds.push(`${ngId}#${labelIndex}`) } @@ -203,6 +206,26 @@ export class AtlasViewerURLService{ }) } + const cViewerState = searchparams.get('cNavigation') + if (cViewerState) { + const [ cO, cPO, cPZ, cP, cZ ] = cViewerState.split(`${separator}${separator}`) + const o = cO.split(separator).map(s => decodeToNumber(s, {float: true})) + const po = cPO.split(separator).map(s => decodeToNumber(s, {float: true})) + const pz = decodeToNumber(cPZ) + const p = cP.split(separator).map(s => decodeToNumber(s)) + const z = decodeToNumber(cZ) + this.store.dispatch({ + type : CHANGE_NAVIGATION, + navigation : { + orientation: o, + perspectiveOrientation: po, + perspectiveZoom: pz, + position: p, + zoom: z + } + }) + } + const niftiLayers = searchparams.get('niftiLayers') if(niftiLayers){ const layers = niftiLayers.split('__') @@ -241,13 +264,23 @@ export class AtlasViewerURLService{ isDefined(state[key].position) && isDefined(state[key].zoom) ){ - _[key] = [ - state[key].orientation.join('_'), - state[key].perspectiveOrientation.join('_'), - state[key].perspectiveZoom, - state[key].position.join('_'), - state[key].zoom - ].join('__') + const { + orientation, + perspectiveOrientation, + perspectiveZoom, + position, + zoom + } = state[key] + + _['cNavigation'] = [ + orientation.map(n => encodeNumber(n, {float: true})).join(separator), + perspectiveOrientation.map(n => encodeNumber(n, {float: true})).join(separator), + encodeNumber(Math.floor(perspectiveZoom)), + Array.from(position).map((v:number) => Math.floor(v)).map(n => encodeNumber(n)).join(separator), + encodeNumber(Math.floor(zoom)) + ].join(`${separator}${separator}`) + + _[key] = null } break; case 'regionsSelected': { @@ -274,7 +307,7 @@ export class AtlasViewerURLService{ for (let entry of ngIdLabelIndexMap) { const [ ngId, labelIndicies ] = entry - returnObj[ngId] = labelIndicies.map(encodeNumber).join(separator) + returnObj[ngId] = labelIndicies.map(n => encodeNumber(n)).join(separator) } _['cRegionsSelected'] = JSON.stringify(returnObj) -- GitLab