-
Notifications
You must be signed in to change notification settings - Fork 18
/
state.js
283 lines (264 loc) · 11.5 KB
/
state.js
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/**
* OneZoom URL Parsing
*
* Functions to parse a *URL string* to/from a *state object*
*
* A OneZoom *URL string* is of the form:
*
* /life/@biota=93302?cols=AT&...#x123...
*
* Where:
* * ``/life/`` is the treeviewer page used
* * ``@biota=93302`` is the pinpoint the treeviewer is focusing on,
* see {@link navigation/pinpoint#resolve_pinpoints} for more detail on the format.
* * ``?cols=AT&..`` is the querystring *treestate*, see below
* * ``#x123`` is fine-grained adjustment of where the focal node should be on-screen.
*
* The treestate querystring can contain any of the following parts, joined by ``&``:
* * ``pop``: Trigger a UI popup, a string of the form "(tab)-(OTT)" e.g. ``pop=ol_417950``
* * ``vis``: The tree visualisation style, e.g. ``vis=polytomy``
* * ``init``: The manner in which to head to the initial node, see {@link controller/controller_anim#init_move_to}, e.g. ``init=pzoom``
* * ``lang``: A 2-letter ISO 639-1 language code, e.g. ``lang=en``
* * ``img``: Image source, 'best_verified', 'best_pd', or 'best_any', e.g. ``img=best_pd``
* * ``anim``: How to navigate to search results, ``anim=jump``, ``anim=flight`` or ``anim=straight``. Optionally add ``-(speed)`` to set relative speed as float.
* * ``otthome``: The "home" pinpoint, i.e. where you go on clicking reset, e.g. ``otthome=@aves``
* * ``ssaver``: Screensaver inactive duration in seconds, e.g. ``ssaver=60``
* * ``highlight``: Highlight strings to apply to a tree, can be used multiple times. See {@link projection/highlight/highlight}, e.g. ``highlight=path:@aves&highlight=path:@mammalia``
* * ``tour``: A tour URL to play, e.g. ``tour=/tour/data.html/superpowers``
*
* As well as in the browser URL, querystring parameters can be used with set_treestate, e.g.
*
* onezoom.controller.set_treestate('?cols=AT');
*
* The following parameters are deprecated:
* * ``initmark``: Equivalent to ``highlight=path:_ozid=(x)``
*
* @module navigation/state
*/
/**
* Parse location into a state object. Location can be one of:
* * A (window.)location or URL object
* * A querystring beginning with ?
* * A string parsable as URL
* * A state object already
* @return state object ready for setup_page_by_state()
*/
function parse_state(location) {
if (typeof location === 'string' && location.startsWith('?')) {
// If location is stringy and starts with ?, it's a query-string
return parse_window_location({ search: location });
}
if (typeof location === 'string' && location.startsWith('@')) {
// If location is stringy and starts with @, it's a pinpoint relative to current page
// Prefix with current page URL and go to that.
location = parse_url_base(window.location) + location;
return parse_window_location(new URL(location));
}
if (typeof location === 'string') {
// Otherwise parse as URL before setting it up
return parse_window_location(new URL(location));
}
if (typeof location !== 'object') {
throw new Error("Cannot parse objects of type " + typeof location)
}
if ((global.Location && location instanceof Location) || location instanceof URL) {
// Parse Location/URL objects
return parse_window_location(location);
}
// All else fails, assume it's a state object already
return location
}
/**
* Parse a DOM location object and return a state object.
*/
function parse_window_location(location) {
let state = {};
if (location.pathname) state.url_base = parse_url_base(location);
parse_pathname(state, location.pathname);
parse_querystring(state, location.search);
parse_hash(state, location.hash);
return state;
}
/**
* Turn a state object back into a location
*/
function deparse_state(state) {
return new URL(deparse_pathname(state) + deparse_querystring(state) + deparse_hash(state));
}
/**
* Parse the URL base from a location object
*/
function parse_url_base(location) {
//find the base path, without the /@Homo_sapiens bit, if it exists
//note that location.pathname does not include ?a=b and #foobar parts
let index = location.pathname.indexOf("@");
if (index === -1) {
return location.origin + location.pathname.replace(/\/*$/, "/");
} else {
return location.origin + location.pathname.substring(0, index).replace(/\/*$/, "/");
}
}
// Pull pinpoint out of pathname, e.g. @Eukaryota=304358
function parse_pathname(state, pathname) {
if (pathname) {
state.pinpoint = pathname.indexOf("@") > -1 ? pathname.replace(/^[^@]*/, '') : null;
}
}
/// pathname should equal pinpoint
function deparse_pathname(state) {
return state.url_base + state.pinpoint;
}
/**
* Parse the query string, then store the result in state object.
* @param {Object} state - the state that will be changed
* @param {String} querystring - the query string. Could be null or undefined or empty, otherwise must start with '?'
and continues until the end or a '#' is reached
*/
function parse_querystring(state, querystring) {
if (typeof querystring !== 'string') return;
querystring = querystring.replace(/^\?/, '').split("&"); //knock off initial '?'
for (let i = 0; i < querystring.length; i++) {
if (/^pop=/.test(querystring[i])) {
state.tap_action = decode_popup_action(querystring[i].substring(querystring[i].indexOf("=") + 1));
} else if (/^vis=/.test(querystring[i])) {
let vis_type = querystring[i].substring(querystring[i].indexOf("=") + 1);
state.vis_type = vis_type;
} else if (/^init=/.test(querystring[i])) {
let init = querystring[i].substring(querystring[i].indexOf("=") + 1);
state.init = init;
} else if (/^lang=/.test(querystring[i])) {
//if the user wants a specific language: not the one given by the browser
let lang = querystring[i].substring(querystring[i].indexOf("=") + 1);
state.lang = lang;
} else if (/^img=/.test(querystring[i])) {
//if the user wants a specific language: not the one given by the browser
let image_source = querystring[i].substring(querystring[i].indexOf("=") + 1);
state.image_source = image_source;
} else if (/^anim=/.test(querystring[i])) {
//if the user wants a specific language: not the one given by the browser
let search_jump_mode = querystring[i].substring(querystring[i].indexOf("=") + 1);
state.search_jump_mode = search_jump_mode;
} else if (/^otthome=/.test(querystring[i])) {
// The location that "reset view" will head to
state.home_ott_id = decodeURIComponent(querystring[i].substring(querystring[i].indexOf("=") + 1));
} else if (/^ssaver=/.test(querystring[i])) {
//if the user wants a specific language: not the one given by the browser
let ssaver_inactive_duration_seconds = querystring[i].substring(querystring[i].indexOf("=") + 1);
state.ssaver_inactive_duration_seconds = ssaver_inactive_duration_seconds;
} else if (/^cols=/.test(querystring[i])) {
// User wants a given colour scheme
state.cols = querystring[i].substring(querystring[i].indexOf("=") + 1);
} else if (/^initmark=/.test(querystring[i])) {
// Decode initmark parameter for backward compatibility
if (!state.highlights) state.highlights = [];
state.highlights.push('path:@_ozid=' + decodeURIComponent(querystring[i].substring(querystring[i].indexOf("=") + 1)));
} else if (/^highlight=/.test(querystring[i])) {
// User wants a highlight marking
if (!state.highlights) state.highlights = [];
state.highlights.push(decodeURIComponent(querystring[i].substring(querystring[i].indexOf("=") + 1)));
// Remove empty highlights, so we can use "?highlights=" to trigger clearing of highlights
if (!state.highlights[state.highlights.length - 1]) state.highlights.pop();
} else if (/^tour=/.test(querystring[i])) {
// User wants a tour
state.tour_setting = decodeURIComponent(querystring[i].substring(querystring[i].indexOf("=") + 1));
}
}
}
function deparse_querystring(state) {
var sp = new URLSearchParams("");
if (state.tap_action) sp.set('pop', encode_popup_action(state.tap_action));
if (state.vis_type) sp.set('vis', state.vis_type);
if (state.init) sp.set('init', state.init);
if (state.lang) sp.set('lang', state.lang);
if (state.image_source) sp.set('img', state.image_source);
if (state.search_jump_mode) sp.set('anim', state.search_jump_mode);
if (state.home_ott_id) sp.set('otthome', state.home_ott_id);
if (state.ssaver_inactive_duration_seconds) sp.set('ssaver', state.ssaver_inactive_duration_seconds);
if (state.cols) sp.set('cols', state.cols);
(state.highlights || []).forEach((x) => sp.append('highlight', x));
if (state.tour_setting) sp.set('tour', state.tour_setting);
if (state.custom_querystring) {
for (let k in state.custom_querystring) sp.set(k, state.custom_querystring[k]);
}
let out = sp.toString();
return out ? '?' + out : out;
}
//Object, String
//This function parse the hash string, then store the result in state object.
//String is one of --
// null or undefined or empty
// '#ott' + Number
// '#' + String(name of species)
// '#x' + Number + ',y' + Number + ',w' + Number
//Parse result is one of --
// null (no change)
// ott: Number
// name: String
// xp: Number, yp: Number, ws: Float
function parse_hash(state, hash) {
if (!hash || hash.length === 0) return;
hash = hash.substring(1); //remove '#'
if (hash.indexOf("ott") === 0) {
let ott = parseInt(hash.substring(3), 10);
if (!isNaN(ott)) state.ott = ott;
} else {
let parts = hash.split(",");
if (parts.length === 3 && hash.indexOf("x") !== -1 && hash.indexOf(",y") !== -1 && hash.indexOf(",w") !== -1) {
for (let i = 0; i < parts.length; i++) {
if (parts[i].match(/x/)) {
state.xp = parseInt(parts[i].substring(parts[i].indexOf("x") + 1));
} else if (parts[i].match(/y/)) {
state.yp = parseInt(parts[i].substring(parts[i].indexOf("y") + 1));
} else if (parts[i].match(/w/)) {
state.ws = parseFloat(parts[i].substring(parts[i].indexOf("w") + 1));
}
}
} else {
state.latin_name = hash;
}
}
}
function deparse_hash(state) {
if (!state.xp || !state.yp || !state.ws) return "";
return "#x" + state.xp.toFixed(0) + ",y" + state.yp.toFixed(0) + ",w" + state.ws.toFixed(4);
}
function encode_popup_action(popup_action) {
if (popup_action.action === "ow_leaf") {
return 'ol_' + popup_action.data;
} else if (popup_action.action === "ow_node") {
return 'on_' + popup_action.data;
} else if (popup_action.action === "ow_ozspons_leaf") {
return 'osl_' + popup_action.data;
} else if (popup_action.action === "ow_ozspons_node") {
return 'osn_' + popup_action.data;
} else if (popup_action.action === "ow_iucn_leaf") {
return 'oil_' + popup_action.data;
}
// Otherwise, use non-abbreviated form
return popup_action.action + '_' + popup_action.data;
}
function decode_popup_action(popup_qs) {
var m;
if (popup_qs.match(/^\d+$/)) {
// Old params specification style. pop=1234
return { action: "ow_leaf", data: parseInt(popup_qs, 10) };
}
m = popup_qs.match(/^(ol|on|osl|osn|oil)_(\d+)$/);
if (m) {
// Abbreviated style, pop=ol=1234
return { action: {
ol: 'ow_leaf',
on: 'ow_node',
osl: 'ow_ozspons_leaf',
osn: 'ow_ozspons_node',
oil: 'ow_iucn_leaf',
}[m[1]], data: parseInt(m[2], 10) };
}
m = popup_qs.match(/^(ow_.*_(?:leaf|node))_(\d+)$/);
if (m) {
// Full style, pop=ow_oztours_leaf_1234
return { action: m[1], data: parseInt(m[2], 10) };
}
throw new Error("Unparsable pop parameter: " + popup_qs);
}
export { parse_state, deparse_state, parse_url_base };