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