forked from FZJ-INM1-BDA/siibra-python
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathurl.py
222 lines (171 loc) · 6.9 KB
/
url.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# Copyright 2018-2024
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional, TYPE_CHECKING
from urllib.parse import quote_plus
from numpy import int32
import numpy as np
import re
from dataclasses import dataclass
import math
from .util import encode_number, separator, cipher, neg, decode_number, post_process
if TYPE_CHECKING:
from siibra.core.atlas import Atlas
from siibra.core.space import Space
from siibra.locations import BoundingBox, Point
from siibra.core.parcellation import Parcellation
from siibra.core.region import Region
from siibra.features.feature import Feature
class DecodeNavigationException(Exception):
pass
min_int32 = -2_147_483_648
max_int32 = 2_147_483_647
default_root_url = "https://atlases.ebrains.eu/viewer/"
def sanitize_id(id: str):
return id.replace("/", ":")
def get_perspective_zoom(
atlas: "Atlas", space: "Space", parc: "Parcellation", region: Optional["Region"]
):
import siibra
if atlas is siibra.atlases["rat"] or atlas is siibra.atlases["mouse"]:
return 200000
return 2000000
def get_zoom(
atlas: "Atlas", space: "Space", parc: "Parcellation", region: Optional["Region"]
):
import siibra
if atlas is siibra.atlases["rat"] or atlas is siibra.atlases["mouse"]:
return 35000
return 350000
supported_prefix = ("nifti://", "swc://", "precomputed://", "deepzoom://")
def append_query_params(url: str, *args, query_params={}, **kwargs):
query_str = "&".join(
[f"{key}={quote_plus(value)}" for key, value in query_params.items()]
)
if len(query_str) > 0:
query_str = "?" + query_str
return url + query_str
@post_process(append_query_params)
def encode_url(
atlas: "Atlas",
space: "Space",
parc: "Parcellation",
region: Optional["Region"] = None,
*,
root_url=default_root_url,
external_url: str = None,
location: "Point" = None,
feature: "Feature" = None,
ignore_warning=False,
query_params={},
):
from siibra.locations import Point
overlay_url = None
encoded_position = None
if location:
assert isinstance(location, Point), "currently, location only supports Point"
encoded_position = ".".join([encode_number(int(p * 1e6)) for p in location])
if external_url:
assert any(
[external_url.startswith(prefix) for prefix in supported_prefix]
), f"url needs to start with {(' , '.join(supported_prefix))}"
overlay_url = "/x-overlay-layer:{url}".format(
url=external_url.replace("/", "%2F")
)
zoom = get_zoom(atlas, space, parc, region)
pzoom = get_perspective_zoom(atlas, space, parc, region)
zoom_kwargs = {
"encoded_pzoom": encode_number(pzoom, False),
"encoded_zoom": encode_number(zoom, False),
}
nav_string = "/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..{encoded_pzoom}..{encoded_nav}..{encoded_zoom}"
return_url = (
"{root_url}#/a:{atlas_id}/t:{template_id}/p:{parc_id}{overlay_url}".format(
root_url=root_url,
atlas_id=sanitize_id(atlas.id),
template_id=sanitize_id(space.id),
parc_id=sanitize_id(parc.id),
overlay_url=overlay_url if overlay_url else "",
)
)
if feature is not None:
return_url = return_url + f"/f:{sanitize_id(feature.id)}"
if region is None:
return return_url + nav_string.format(encoded_nav=encoded_position or "0.0.0", **zoom_kwargs)
return_url = f"{return_url}/rn:{get_hash(region.name)}"
try:
result_props = region.spatial_props(space, maptype="labelled")
if len(result_props.components) == 0:
return return_url + nav_string.format(encoded_nav=encoded_position or "0.0.0", **zoom_kwargs)
except Exception as e:
print(f"Cannot get_spatial_props {str(e)}")
if not ignore_warning:
raise e
return return_url + nav_string.format(encoded_nav=encoded_position or "0.0.0", **zoom_kwargs)
centroid = result_props.components[0].centroid
encoded_centroid = separator.join(
[encode_number(math.floor(val * 1e6)) for val in centroid]
)
return_url = return_url + nav_string.format(
encoded_nav=encoded_position or encoded_centroid, **zoom_kwargs
)
return return_url
@dataclass
class DecodedUrl:
bounding_box: "BoundingBox"
def decode_url(url: str, vp_length=1000):
import siibra
try:
space_match = re.search(r"/t:(?P<space_id>[^/]+)", url)
space_id = space_match.group("space_id")
space_id = space_id.replace(":", "/")
space = siibra.spaces[space_id]
except Exception as e:
raise DecodeNavigationException from e
nav_match = re.search(r"/@:(?P<navigation_str>.+)/?", url)
navigation_str = nav_match.group("navigation_str")
for char in navigation_str:
assert char in cipher or char in [
neg,
separator,
], f"char {char} not in cipher, nor separator/neg"
try:
ori_enc, pers_ori_enc, pers_zoom_enc, pos_enc, zoomm_enc = navigation_str.split(
f"{separator}{separator}"
)
except Exception as e:
raise DecodeNavigationException from e
try:
x_enc, y_enc, z_enc = pos_enc.split(separator)
pos = [decode_number(val) for val in [x_enc, y_enc, z_enc]]
zoom = decode_number(zoomm_enc)
# zoom = nm/pixel
pt1 = [(coord - (zoom * vp_length / 2)) / 1e6 for coord in pos]
pt1 = Point(pt1, space)
pt2 = [(coord + (zoom * vp_length / 2)) / 1e6 for coord in pos]
pt2 = Point(pt2, space)
except Exception as e:
raise DecodeNavigationException from e
bbx = BoundingBox(pt1, pt2, space)
return DecodedUrl(bounding_box=bbx)
def get_hash(full_string: str):
return_val = 0
with np.errstate(over="ignore"):
for char in full_string:
# overflowing is expected and in fact the whole reason why convert number to int32
# in windows, int32((0 - min_int32) << 5), rather than overflow to wrapper around, raises OverflowError
shifted_5 = int32(
(return_val - min_int32) if return_val > max_int32 else return_val << 5
)
return_val = int32(shifted_5 - return_val + ord(char))
return_val = return_val & return_val
hex_val = hex(return_val)
return hex_val[3:]