From 6922265f6afa0022e7386bb976eeceab1822a3a0 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Mon, 1 Jul 2019 11:19:27 +0200
Subject: [PATCH] feat: encodding region selected as b64, greatly reduce url
 lengtth

comparison allen v3, ~580 regions selected:
- pre 2019 May release: 2854 char
- post 2019 May release: 7390 char
- this patch: 1997 char

maintaining backwards compat
---
 .../atlasViewer.constantService.service.ts    | 53 ++++++++++++++-
 .../atlasViewer.urlService.service.ts         | 64 +++++++++++++++++--
 2 files changed, 112 insertions(+), 5 deletions(-)

diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts
index 75a157e0c..22511be58 100644
--- a/src/atlasViewer/atlasViewer.constantService.service.ts
+++ b/src/atlasViewer/atlasViewer.constantService.service.ts
@@ -290,4 +290,55 @@ 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 = "."
+
+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"
+
+  let rixit // like 'digit', only in some non-decimal radix 
+  let residual = Math.floor(number)
+  let result = ''
+  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
+}
+
+export const decodeToNumber = (encodedString: string) => {
+  return [...encodedString].reduce((acc,curr) => {
+    return acc * 64 + cipher.indexOf(curr)
+  }, 0)
+}
\ No newline at end of file
diff --git a/src/atlasViewer/atlasViewer.urlService.service.ts b/src/atlasViewer/atlasViewer.urlService.service.ts
index b548c96f2..db47cabb7 100644
--- a/src/atlasViewer/atlasViewer.urlService.service.ts
+++ b/src/atlasViewer/atlasViewer.urlService.service.ts
@@ -1,11 +1,11 @@
 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";
 
@@ -157,6 +157,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(decodeToNumber)
+              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 */
@@ -222,9 +250,37 @@ export class AtlasViewerURLService{
                     ].join('__')
                   }
                   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(encodeNumber).join(separator)
+                  }
+                  
+                  _['cRegionsSelected'] = JSON.stringify(returnObj)
+                  _[key] = null
                   break;
+                }
                 case 'templateSelected':
                 case 'parcellationSelected':
                   _[key] = state[key].name
-- 
GitLab