Skip to content
Snippets Groups Projects
Unverified Commit 447459b7 authored by xgui3783's avatar xgui3783 Committed by GitHub
Browse files

Merge pull request #241 from HumanBrainProject/feat_useCipherForUrlEncoding

feat: encodding region selected as b64, greatly reduce url lengtth
parents 20139a58 075dde8e
No related merge requests found
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
......@@ -290,4 +290,98 @@ export const SUPPORT_LIBRARY_MAP : Map<string,HTMLElement> = new Map([
['vue@2.5.16',parseURLToElement('https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js')],
['preact@8.4.2',parseURLToElement('https://cdn.jsdelivr.net/npm/preact@8.4.2/dist/preact.min.js')],
['d3@5.7.0',parseURLToElement('https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js')]
])
\ No newline at end of file
])
/**
* First attempt at encoding int (e.g. selected region, navigation location) from number (loc info density) to b64 (higher info density)
* The constraint is that the cipher needs to be commpatible with URI encoding
* and a URI compatible separator is required.
*
* The implementation below came from
* https://stackoverflow.com/a/6573119/6059235
*
* While a faster solution exist in the same post, this operation is expected to be done:
* - once per 1 sec frequency
* - on < 1000 numbers
*
* 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 '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
let result = ''
if (number < 0) {
result += negString
residual = Math.floor(number * -1)
} else {
residual = Math.floor(number)
}
while (true) {
rixit = residual % 64
// console.log("rixit : " + rixit)
// console.log("result before : " + result)
result = cipher.charAt(rixit) + result
// console.log("result after : " + result)
// console.log("residual before : " + residual)
residual = Math.floor(residual / 64)
// console.log("residual after : " + residual)
if (residual == 0)
break;
}
return result
}
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) === negString) {
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
import { Injectable } from "@angular/core";
import { Store, select } from "@ngrx/store";
import { ViewerStateInterface, isDefined, NEWVIEWER, CHANGE_NAVIGATION, ADD_NG_LAYER, generateLabelIndexId } from "../services/stateStore.service";
import { ViewerStateInterface, isDefined, NEWVIEWER, CHANGE_NAVIGATION, ADD_NG_LAYER } from "../services/stateStore.service";
import { PluginInitManifestInterface } from 'src/services/state/pluginState.store'
import { Observable,combineLatest } from "rxjs";
import { filter, map, scan, distinctUntilChanged, skipWhile, take } from "rxjs/operators";
import { PluginServices } from "./atlasViewer.pluginService.service";
import { AtlasViewerConstantsServices } from "./atlasViewer.constantService.service";
import { AtlasViewerConstantsServices, encodeNumber, separator, decodeToNumber } from "./atlasViewer.constantService.service";
import { ToastService } from "src/services/toastService.service";
import { SELECT_REGIONS_WITH_ID } from "src/services/state/viewerState.store";
......@@ -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('_')
......@@ -157,6 +160,34 @@ export class AtlasViewerURLService{
selectRegionIds: ids
})
}
const cRegionsSelectedParam = searchparams.get('cRegionsSelected')
if (cRegionsSelectedParam) {
try {
const json = JSON.parse(cRegionsSelectedParam)
const selectRegionIds = []
for (let ngId in json) {
const val = json[ngId]
const labelIndicies = val.split(separator).map(n =>decodeToNumber(n))
for (let labelIndex of labelIndicies) {
selectRegionIds.push(`${ngId}#${labelIndex}`)
}
}
this.store.dispatch({
type: SELECT_REGIONS_WITH_ID,
selectRegionIds
})
} catch (e) {
/**
* parsing cRegionSelected error
*/
console.log('parsing cRegionSelected error', e)
}
}
}
/* now that the parcellation is loaded, load the navigation state */
......@@ -175,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('__')
......@@ -213,18 +264,56 @@ 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':
_[key] = state[key].map(({ ngId, labelIndex })=> generateLabelIndexId({ ngId,labelIndex })).join('_')
case 'regionsSelected': {
// _[key] = state[key].map(({ ngId, labelIndex })=> generateLabelIndexId({ ngId,labelIndex })).join('_')
const ngIdLabelIndexMap : Map<string, number[]> = state[key].reduce((acc, curr) => {
const returnMap = new Map(acc)
const { ngId, labelIndex } = curr
const existingArr = (returnMap as Map<string, number[]>).get(ngId)
if (existingArr) {
existingArr.push(labelIndex)
} else {
returnMap.set(ngId, [labelIndex])
}
return returnMap
}, new Map())
if (ngIdLabelIndexMap.size === 0) {
_['cRegionsSelected'] = null
_[key] = null
break;
}
const returnObj = {}
for (let entry of ngIdLabelIndexMap) {
const [ ngId, labelIndicies ] = entry
returnObj[ngId] = labelIndicies.map(n => encodeNumber(n)).join(separator)
}
_['cRegionsSelected'] = JSON.stringify(returnObj)
_[key] = null
break;
}
case 'templateSelected':
case 'parcellationSelected':
_[key] = state[key].name
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment