xref: /online/loleaflet/src/control/Control.StatusBar.js (revision 4fa5b43b628152eeca05684ff6704f88d0445f04)
1/* -*- js-indent-level: 8 -*- */
2/*
3 * L.Control.StatusBar
4 */
5
6/* global $ w2ui _ _UNO */
7L.Control.StatusBar = L.Control.extend({
8
9	initialize: function () {
10	},
11
12	onAdd: function (map) {
13		this.map = map;
14		map.on('doclayerinit', this.onDocLayerInit, this);
15		map.on('commandvalues', this.onCommandValues, this);
16		map.on('commandstatechanged', this.onCommandStateChanged, this);
17		this.create();
18
19		$(window).resize(function() {
20			if ($(window).width() !== map.getSize().x) {
21				var statusbar = w2ui['actionbar'];
22				statusbar.resize();
23			}
24		});
25	},
26
27	hideTooltip: function(toolbar, id) {
28		if (toolbar.touchStarted) {
29			setTimeout(function() {
30				toolbar.tooltipHide(id, {});
31			}, 5000);
32			toolbar.touchStarted = false;
33		}
34	},
35
36	updateToolbarItem: function(toolbar, id, html) {
37		var item = toolbar.get(id);
38		if (item) {
39			item.html = html;
40		}
41	},
42
43	localizeStateTableCell: function(text) {
44		var stateArray = text.split(';');
45		var stateArrayLength = stateArray.length;
46		var localizedText = '';
47		for (var i = 0; i < stateArrayLength; i++) {
48			var labelValuePair = stateArray[i].split(':');
49			localizedText += _(labelValuePair[0].trim()) + ':' + labelValuePair[1];
50			if (stateArrayLength > 1 && i < stateArrayLength - 1) {
51				localizedText += '; ';
52			}
53		}
54		return localizedText;
55	},
56
57	toLocalePattern: function(pattern, regex, text, sub1, sub2) {
58		var matches = new RegExp(regex, 'g').exec(text);
59		if (matches) {
60			text = pattern.toLocaleString().replace(sub1, parseInt(matches[1].replace(',','')).toLocaleString(String.locale)).replace(sub2, parseInt(matches[2].replace(',','')).toLocaleString(String.locale));
61		}
62		return text;
63	},
64
65	_updateVisibilityForToolbar: function(toolbar) {
66		if (!toolbar)
67			return;
68
69		var toShow = [];
70		var toHide = [];
71
72		toolbar.items.forEach(function(item) {
73			if (window.ThisIsTheiOSApp && window.mode.isTablet() && item.iosapptablet === false) {
74				toHide.push(item.id);
75			}
76			else if (((window.mode.isMobile() && item.mobile === false) || (window.mode.isTablet() && item.tablet === false) || (window.mode.isDesktop() && item.desktop === false) || (!window.ThisIsAMobileApp && item.mobilebrowser === false)) && !item.hidden) {
77				toHide.push(item.id);
78			}
79			else if (((window.mode.isMobile() && item.mobile === true) || (window.mode.isTablet() && item.tablet === true) || (window.mode.isDesktop() && item.desktop === true) || (window.ThisIsAMobileApp && item.mobilebrowser === true)) && item.hidden) {
80				toShow.push(item.id);
81			}
82		});
83
84		console.log('explicitly hiding: ' + toHide);
85		console.log('explicitly showing: ' + toShow);
86
87		toHide.forEach(function(item) { toolbar.hide(item); });
88		toShow.forEach(function(item) { toolbar.show(item); });
89	},
90
91	_updateToolbarsVisibility: function() {
92		this._updateVisibilityForToolbar(w2ui['actionbar']);
93	},
94
95	onClick: function(e, id, item, subItem) {
96		if ('actionbar' in w2ui && w2ui['actionbar'].get(id) !== null) {
97			var toolbar = w2ui['actionbar'];
98			item = toolbar.get(id);
99		}
100
101		// In the iOS app we don't want clicking on the toolbar to pop up the keyboard.
102		if (!window.ThisIsTheiOSApp && id !== 'zoomin' && id !== 'zoomout' && id !== 'mobile_wizard' && id !== 'insertion_mobile_wizard') {
103			this.map.focus(this.map.canAcceptKeyboardInput()); // Maintain same keyboard state.
104		}
105
106		if (item.disabled) {
107			return;
108		}
109
110		var docLayer = this.map._docLayer;
111
112		if (item.uno) {
113			if (item.unosheet && this.map.getDocType() === 'spreadsheet') {
114				this.map.toggleCommandState(item.unosheet);
115			}
116			else {
117				this.map.toggleCommandState(window.getUNOCommand(item.uno));
118			}
119		}
120		else if (id === 'zoomin' && this.map.getZoom() < this.map.getMaxZoom()) {
121			this.map.zoomIn(1);
122		}
123		else if (id === 'zoomout' && this.map.getZoom() > this.map.getMinZoom()) {
124			this.map.zoomOut(1);
125		}
126		else if (item.scale) {
127			this.map.setZoom(item.scale);
128		}
129		else if (id === 'zoomreset') {
130			this.map.setZoom(this.map.options.zoom);
131		}
132		else if (id === 'prev' || id === 'next') {
133			if (docLayer._docType === 'text') {
134				this.map.goToPage(id);
135			}
136			else {
137				this.map.setPart(id);
138			}
139		}
140		else if (id === 'searchprev') {
141			this.map.search(L.DomUtil.get('search-input').value, true);
142		}
143		else if (id === 'searchnext') {
144			this.map.search(L.DomUtil.get('search-input').value);
145		}
146		else if (id === 'cancelsearch') {
147			this._cancelSearch();
148		}
149		else if (id.startsWith('StateTableCellMenu') && subItem) {
150			e.done(function () {
151				var menu = w2ui['actionbar'].get('StateTableCellMenu');
152				if (subItem.id === '1') { // 'None' was clicked, remove all other options
153					menu.selected = ['1'];
154				}
155				else { // Something else was clicked, remove the 'None' option from the array
156					var index = menu.selected.indexOf('1');
157					if (index > -1) {
158						menu.selected.splice(index, 1);
159					}
160				}
161				var value = 0;
162				for (var it = 0; it < menu.selected.length; it++) {
163					value = +value + parseInt(menu.selected[it]);
164				}
165				var command = {
166					'StatusBarFunc': {
167						type: 'unsigned short',
168						value: value
169					}
170				};
171				this.map.sendUnoCommand('.uno:StatusBarFunc', command);
172			}.bind(this));
173		}
174		else if (id === 'userlist') {
175			this.map.fire('openuserlist');
176		}
177	},
178
179	create: function() {
180		var toolbar = $('#toolbar-down');
181		var that = this;
182
183		if (!window.mode.isMobile()) {
184			toolbar.w2toolbar({
185				name: 'actionbar',
186				items: [
187					{type: 'html',  id: 'search',
188					html: '<div style="padding: 3px 5px 3px 10px;" class="loleaflet-font">' +
189					'<input size="15" id="search-input" placeholder="' + _('Search') + '"' +
190					'style="padding: 3px; border-radius: 2px; border: 1px solid silver"/>' +
191					'</div>'
192					},
193					{type: 'button',  id: 'searchprev', img: 'prev', hint: _UNO('.uno:UpSearch'), disabled: true},
194					{type: 'button',  id: 'searchnext', img: 'next', hint: _UNO('.uno:DownSearch'), disabled: true},
195					{type: 'button',  id: 'cancelsearch', img: 'cancel', hint: _('Cancel the search'), hidden: true},
196					{type: 'html',  id: 'left'},
197					{type: 'html',  id: 'right'},
198					{type: 'drop', id: 'userlist', img: 'users', hidden: true, html: L.control.createUserListWidget()},
199					{type: 'break', id: 'userlistbreak', hidden: true, mobile: false },
200					{type: 'button',  id: 'prev', img: 'prev', hint: _UNO('.uno:PageUp', 'text')},
201					{type: 'button',  id: 'next', img: 'next', hint: _UNO('.uno:PageDown', 'text')},
202					{type: 'break', id: 'prevnextbreak'},
203				].concat(window.mode.isTablet() ? [] : [
204					{type: 'button',  id: 'zoomreset', img: 'zoomreset', hint: _('Reset zoom')},
205					{type: 'button',  id: 'zoomout', img: 'zoomout', hint: _UNO('.uno:ZoomMinus')},
206					{type: 'menu-radio', id: 'zoom', text: '100',
207						selected: 'zoom100',
208						mobile: false,
209						items: [
210							{ id: 'zoom20', text: '20', scale: 1},
211							{ id: 'zoom25', text: '25', scale: 2},
212							{ id: 'zoom30', text: '30', scale: 3},
213							{ id: 'zoom35', text: '35', scale: 4},
214							{ id: 'zoom40', text: '40', scale: 5},
215							{ id: 'zoom50', text: '50', scale: 6},
216							{ id: 'zoom60', text: '60', scale: 7},
217							{ id: 'zoom70', text: '70', scale: 8},
218							{ id: 'zoom85', text: '85', scale: 9},
219							{ id: 'zoom100', text: '100', scale: 10},
220							{ id: 'zoom120', text: '120', scale: 11},
221							{ id: 'zoom150', text: '150', scale: 12},
222							{ id: 'zoom175', text: '175', scale: 13},
223							{ id: 'zoom200', text: '200', scale: 14},
224							{ id: 'zoom235', text: '235', scale: 15},
225							{ id: 'zoom280', text: '280', scale: 16},
226							{ id: 'zoom335', text: '335', scale: 17},
227							{ id: 'zoom400', text: '400', scale: 18},
228						]
229					},
230					{type: 'button',  id: 'zoomin', img: 'zoomin', hint: _UNO('.uno:ZoomPlus')}
231				]),
232				onClick: function (e) {
233					that.hideTooltip(this, e.target);
234					that.onClick(e, e.target, e.item, e.subItem);
235				},
236				onRefresh: function() {
237					$('#tb_actionbar_item_userlist .w2ui-tb-caption').addClass('loleaflet-font');
238					window.setupSearchInput();
239				}
240			});
241			if (window.mode.isDesktop())
242				toolbar.tooltip();
243		}
244
245		toolbar.bind('touchstart', function() {
246			w2ui['actionbar'].touchStarted = true;
247		});
248
249		this.map.on('zoomend', function () {
250			var zoomPercent = 100;
251			var zoomSelected = null;
252			switch (that.map.getZoom()) {
253			case 1:  zoomPercent =  20; zoomSelected = 'zoom20'; break;  // 0.2102
254			case 2:  zoomPercent =  25; zoomSelected = 'zoom25'; break;  // 0.2500
255			case 3:  zoomPercent =  30; zoomSelected = 'zoom30'; break;  // 0.2973
256			case 4:  zoomPercent =  35; zoomSelected = 'zoom35'; break;  // 0.3535
257			case 5:  zoomPercent =  40; zoomSelected = 'zoom40'; break;  // 0.4204
258			case 6:  zoomPercent =  50; zoomSelected = 'zoom50'; break;  // 0.5
259			case 7:  zoomPercent =  60; zoomSelected = 'zoom60'; break;  // 0.5946
260			case 8:  zoomPercent =  70; zoomSelected = 'zoom70'; break;  // 0.7071
261			case 9:  zoomPercent =  85; zoomSelected = 'zoom85'; break;  // 0.8409
262			case 10: zoomPercent = 100; zoomSelected = 'zoom100'; break; // 1
263			case 11: zoomPercent = 120; zoomSelected = 'zoom120'; break; // 1.1892
264			// Why do we call this 150% even if it is actually closer to 140%
265			case 12: zoomPercent = 150; zoomSelected = 'zoom150'; break; // 1.4142
266			case 13: zoomPercent = 170; zoomSelected = 'zoom170'; break; // 1.6818
267			case 14: zoomPercent = 200; zoomSelected = 'zoom200'; break; // 2
268			case 15: zoomPercent = 235; zoomSelected = 'zoom235'; break; // 2.3784
269			case 16: zoomPercent = 280; zoomSelected = 'zoom280'; break; // 2.8284
270			case 17: zoomPercent = 335; zoomSelected = 'zoom335'; break; // 3.3636
271			case 18: zoomPercent = 400; zoomSelected = 'zoom400'; break; // 4
272			default:
273				var zoomRatio = that.map.getZoomScale(that.map.getZoom(), that.map.options.zoom);
274				zoomPercent = Math.round(zoomRatio * 100);
275				break;
276			}
277			w2ui['actionbar'].set('zoom', {text: zoomPercent, selected: zoomSelected});
278		});
279	},
280
281	onDocLayerInit: function () {
282		var statusbar = w2ui['actionbar'];
283		var docType = this.map.getDocType();
284
285		switch (docType) {
286		case 'spreadsheet':
287			if (statusbar)
288				statusbar.remove('prev', 'next', 'prevnextbreak');
289
290			if (!window.mode.isMobile()) {
291				statusbar.insert('left', [
292					{type: 'break', id: 'break1'},
293					{
294						type: 'html', id: 'StatusDocPos',
295						html: '<div id="StatusDocPos" class="loleaflet-font" title="' + _('Number of Sheets') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
296					},
297					{type: 'break', id: 'break2'},
298					{
299						type: 'html', id: 'RowColSelCount',
300						html: '<div id="RowColSelCount" class="loleaflet-font" title="' + _('Selected range of cells') + '" style="padding: 5px 5px;line-height:0;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
301					},
302					{type: 'break', id: 'break3', tablet: false},
303					{
304						type: 'html', id: 'InsertMode', mobile: false, tablet: false,
305						html: '<div id="InsertMode" class="loleaflet-font" title="' + _('Entering text mode') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
306					},
307					{type: 'break', id: 'break4', tablet: false},
308					{type: 'menu-radio', id: 'LanguageStatus',
309						mobile: false
310					},
311					{type: 'break', id: 'break5', tablet: false},
312					{
313						type: 'html', id: 'StatusSelectionMode', mobile: false, tablet: false,
314						html: '<div id="StatusSelectionMode" class="loleaflet-font" title="' + _('Selection Mode') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
315					},
316					{type: 'break', id: 'break8', mobile: false, tablet: false},
317					{
318						type: 'html', id: 'StateTableCell', mobile: false, tablet: false,
319						html: '<div id="StateTableCell" class="loleaflet-font" title="' + _('Choice of functions') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
320					},
321					{
322						type: 'menu-check', id: 'StateTableCellMenu', caption: '', selected: ['2', '512'], items: [
323							{id: '2', text: _('Average')},
324							{id: '8', text: _('CountA')},
325							{id: '4', text: _('Count')},
326							{id: '16', text: _('Maximum')},
327							{id: '32', text: _('Minimum')},
328							{id: '512', text: _('Sum')},
329							{id: '8192', text: _('Selection count')},
330							{id: '1', text: _('None')}
331						], tablet: false
332					},
333					{type: 'break', id: 'break9', mobile: false}
334				]);
335			}
336			break;
337
338		case 'text':
339			if (!window.mode.isMobile()) {
340				statusbar.insert('left', [
341					{type: 'break', id: 'break1'},
342					{
343						type: 'html', id: 'StatePageNumber',
344						html: '<div id="StatePageNumber" class="loleaflet-font" title="' + _('Number of Pages') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
345					},
346					{type: 'break', id: 'break2'},
347					{
348						type: 'html', id: 'StateWordCount', mobile: false, tablet: false,
349						html: '<div id="StateWordCount" class="loleaflet-font" title="' + _('Word Counter') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
350					},
351					{type: 'break', id: 'break5', mobile: false, tablet: false},
352					{
353						type: 'html', id: 'InsertMode', mobile: false, tablet: false,
354						html: '<div id="InsertMode" class="loleaflet-font" title="' + _('Entering text mode') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
355					},
356					{type: 'break', id: 'break6', mobile: false, tablet: false},
357					{
358						type: 'html', id: 'StatusSelectionMode', mobile: false, tablet: false,
359						html: '<div id="StatusSelectionMode" class="loleaflet-font" title="' + _('Selection Mode') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
360					},
361					{type: 'break', id: 'break7', mobile: false, tablet: false},
362					{type: 'menu-radio', id: 'LanguageStatus',
363						mobile: false
364					},
365					{type: 'break', id: 'break8', mobile: false}
366				]);
367			}
368			break;
369
370		case 'presentation':
371			if (!window.mode.isMobile()) {
372				statusbar.insert('left', [
373					{type: 'break', id: 'break1'},
374					{
375						type: 'html', id: 'PageStatus',
376						html: '<div id="PageStatus" class="loleaflet-font" title="' + _('Number of Slides') + '" style="padding: 5px 5px;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp</div>'
377					},
378					{type: 'break', id: 'break2', mobile: false, tablet: false},
379					{type: 'menu-radio', id: 'LanguageStatus',
380						mobile: false
381					},
382					{type: 'break', id: 'break8', mobile: false}
383				]);
384			}
385
386		// FALLTHROUGH intended
387		case 'drawing':
388			if (statusbar)
389				statusbar.show('prev', 'next');
390			break;
391		}
392
393		this.map.fire('updateuserlistcount');
394
395		this._updateToolbarsVisibility();
396
397		if (statusbar)
398			statusbar.refresh();
399
400		var showStatusbar = this.map.uiManager.getSavedStateOrDefault('ShowStatusbar');
401		if (showStatusbar)
402			$('#toolbar-down').show();
403		else
404			this.map.uiManager.hideStatusBar(true);
405	},
406
407	_cancelSearch: function() {
408		var toolbar = window.mode.isMobile() ? w2ui['searchbar'] : w2ui['actionbar'];
409		var searchInput = L.DomUtil.get('search-input');
410		this.map.resetSelection();
411		toolbar.hide('cancelsearch');
412		toolbar.disable('searchprev');
413		toolbar.disable('searchnext');
414		searchInput.value = '';
415		if (window.mode.isMobile()) {
416			searchInput.focus();
417			// odd, but on mobile we need to invoke it twice
418			toolbar.hide('cancelsearch');
419		}
420
421		this.map._onGotFocus();
422	},
423
424	onCommandStateChanged: function(e) {
425		var statusbar = w2ui['actionbar'];
426		var commandName = e.commandName;
427		var state = e.state;
428
429		if (!commandName)
430			return;
431
432		if (commandName === '.uno:StatusDocPos') {
433			state = this.toLocalePattern('Sheet %1 of %2', 'Sheet (\\d+) of (\\d+)', state, '%1', '%2');
434			this.updateToolbarItem(statusbar, 'StatusDocPos', $('#StatusDocPos').html(state ? state : '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp').parent().html());
435		}
436		else if (commandName === '.uno:LanguageStatus') {
437			var code = state;
438			var language = _(state);
439			var split = code.split(';');
440			if (split.length > 1) {
441				language = _(split[0]);
442				code = split[1];
443			}
444			w2ui['actionbar'].set('LanguageStatus', {text: language, selected: language});
445		}
446		else if (commandName === '.uno:RowColSelCount') {
447			state = this.toLocalePattern('$1 rows, $2 columns selected', '(\\d+) rows, (\\d+) columns selected', state, '$1', '$2');
448			state = this.toLocalePattern('$1 of $2 records found', '(\\d+) of (\\d+) records found', state, '$1', '$2');
449			this.updateToolbarItem(statusbar, 'RowColSelCount', $('#RowColSelCount').html(state ? state : '<span class="ToolbarStatusInactive">&nbsp;Select multiple cells&nbsp;</span>').parent().html());
450		}
451		else if (commandName === '.uno:InsertMode') {
452			this.updateToolbarItem(statusbar, 'InsertMode', $('#InsertMode').html(state ? L.Styles.insertMode[state].toLocaleString() : '<span class="ToolbarStatusInactive">&nbsp;Insert mode: inactive&nbsp;</span>').parent().html());
453
454			if (!state && this.map.hyperlinkPopup) {
455				this.map.hyperlinkUnderCursor = null;
456				this.map.closePopup(this.map.hyperlinkPopup);
457				this.map.hyperlinkPopup = null;
458			}
459		}
460		else if (commandName === '.uno:StatusSelectionMode' ||
461				commandName === '.uno:SelectionMode') {
462			this.updateToolbarItem(statusbar, 'StatusSelectionMode', $('#StatusSelectionMode').html(state ? L.Styles.selectionMode[state].toLocaleString() : '<span class="ToolbarStatusInactive">&nbsp;Selection mode: inactive&nbsp;</span>').parent().html());
463		}
464		else if (commandName == '.uno:StateTableCell') {
465			this.updateToolbarItem(statusbar, 'StateTableCell', $('#StateTableCell').html(state ? this.localizeStateTableCell(state) : '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp').parent().html());
466		}
467		else if (commandName === '.uno:StatusBarFunc') {
468			var item = statusbar.get('StateTableCellMenu');
469			if (item) {
470				item.selected = [];
471				// Check 'None' even when state is 0
472				if (state === '0') {
473					state = 1;
474				}
475				for (var it = 0; it < item.items.length; it++) {
476					if (item.items[it].id & state) {
477						item.selected.push(item.items[it].id);
478					}
479				}
480			}
481		}
482		else if (commandName === '.uno:StatePageNumber') {
483			state = this.toLocalePattern('Page %1 of %2', 'Page (\\d+) of (\\d+)', state, '%1', '%2');
484			this.updateToolbarItem(statusbar, 'StatePageNumber', $('#StatePageNumber').html(state ? state : '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp').parent().html());
485		}
486		else if (commandName === '.uno:StateWordCount') {
487			state = this.toLocalePattern('%1 words, %2 characters', '([\\d,]+) words, ([\\d,]+) characters', state, '%1', '%2');
488			this.updateToolbarItem(statusbar, 'StateWordCount', $('#StateWordCount').html(state ? state : '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp').parent().html());
489		}
490		else if (commandName === '.uno:PageStatus') {
491			state = this.toLocalePattern('Slide %1 of %2', 'Slide (\\d+) of (\\d+)', state, '%1', '%2');
492			this.updateToolbarItem(statusbar, 'PageStatus', $('#PageStatus').html(state ? state : '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp').parent().html());
493		}
494	},
495
496	onCommandValues: function(e) {
497		if (e.commandName === '.uno:LanguageStatus' && L.Util.isArray(e.commandValues)) {
498			var translated, neutral;
499			var constLang = '.uno:LanguageStatus?Language:string=';
500			var constDefault = 'Default_RESET_LANGUAGES';
501			var constNone = 'Default_LANGUAGE_NONE';
502			var resetLang = _('Reset to Default Language');
503			var noneLang = _('None (Do not check spelling)');
504			var languages = [];
505			e.commandValues.forEach(function (language) {
506				languages.push({ translated: _(language.split(';')[0]), neutral: language });
507			});
508			languages.sort(function (a, b) {
509				return a.translated < b.translated ? -1 : a.translated > b.translated ? 1 : 0;
510			});
511
512			var toolbaritems = [];
513			toolbaritems.push({ text: noneLang,
514			 id: 'nonelanguage',
515			 uno: constLang + constNone });
516
517
518			for (var lang in languages) {
519				translated = languages[lang].translated;
520				neutral = languages[lang].neutral;
521				var splitNeutral = neutral.split(';');
522				toolbaritems.push({ id: neutral, text: translated, uno: constLang + encodeURIComponent('Default_' + splitNeutral[0]) });
523			}
524
525			toolbaritems.push({ id: 'reset', text: resetLang, uno: constLang + constDefault });
526
527			w2ui['actionbar'].set('LanguageStatus', {items: toolbaritems});
528		}
529	},
530});
531
532L.control.statusBar = function () {
533	return new L.Control.StatusBar();
534};
535