PageSourceSearch

https://www.fnab.org/wp-content/plugins/mapplic/core/mapplic.js?ver=7.1

js fnab.org collected 2026-09-24 19:19:46 UTC 82,029 bytes, 2,561 lines download raw bytes

1/*
2 * Mapplic - Custom Interactive Map Plugin by @sekler
3 * Version 7.1.2
4 * https://www.mapplic.com/
5 */
6
7;(function($) {
8	"use strict";
9
10	var Mapplic = function(element) {
11
12		var self = this;
13
14		self.o = {
15			source: 'locations.json',
16			selector: '[id^=MLOC] > *, [id^=landmark] > *, svg > #items > *',
17			external: '.mapplic-external',
18			scope: null,
19			customcss: null,
20			iconfile: null,
21			height: 'auto',
22			minheight: 400,
23			maxheight: 800,
24			csv: false,
25			landmark: false,
26			portrait: 860,
27			minimap: false,
28			deeplinking: true,
29			lightbox: true,
30			fullscreen: false,
31			hovertip: true,
32			defaultstyle: null,
33			moretext: null,
34			action: 'default',
35			marker: '',
36			developer: false,
37			smartip: false,
38			animations: false,
39			hovertipdesc: false,
40
41			// sidebar
42			sidebar: true,
43			sidebartoggle: false,
44			filtersopened: false,
45			search: true,
46			searchlength: 1,
47			searchfields: ['title', 'about', 'category'],
48			searchdescription: false,
49			highlight: true,
50			thumbholder: false,
51			autopopulate: false,
52			sortby: 'title',
53
54			// zoom
55			zoom: true,
56			clearbutton: true,
57			zoombuttons: true,
58			zoomoutclose: false,
59			closezoomout: true,
60			linknewtab: false,
61			mousewheel: true,
62			mapfill: false,
63			zoommargin: 200,
64			maxscale: 3,
65
66			// UI Colors
67			basecolor: null,
68			bgcolor: null,
69			bgcolor2: null,
70			headingcolor: null,
71			textcolor: null,
72			accentcolor: null
73		};
74
75		self.loc = {
76			more: 'More',
77			search: 'Search',
78			zoomin: 'Zoom in',
79			zoomout: 'Zoom out',
80			resetzoom: 'Reset zoom',
81			levelup: 'Level up',
82			leveldown: 'Level down',
83			clearsearch: 'Clear search',
84			closepopup: 'Close popup',
85			clearfilter: 'Clear filter',
86			iconfile: 'mapplic/images/icons.svg'
87		}
88
89		self.el = element;
90
91		self.init = function(options) {
92
93			// merging options with defaults
94			self.o = $.extend(self.o, options);
95			if (typeof mapplic_localization !== 'undefined') self.loc = $.extend(self.loc, mapplic_localization);
96			if (self.o.iconfile) self.loc.iconfile = self.o.iconfile;
97
98			self.el.addClass('mapplic-element mapplic-loading');
99
100			// trigger event
101			self.el.trigger('mapload', self);
102			
103			// scope
104			if (self.o.scope) self.scope = $(self.o.scope);
105			else self.scope = self.el;
106
107			// process map data
108			var data = self.el.data('mapdata');
109			if (self.el.data('mapdata')) {
110				var mapdata = self.el.data('mapdata');
111				self.el.removeAttr('data-mapdata').removeData('mapdata');
112				processData(mapdata);
113				self.el.removeClass('mapplic-loading');
114			}
115			else if (typeof self.o.source === 'string') {
116				// loading .json file with AJAX
117				$.getJSON(self.o.source, function(data) {
118					processData(data);
119					self.el.removeClass('mapplic-loading');
120				}).fail(function() { // Failure: couldn't load JSON file or it is invalid.
121					console.error('Couldn\'t load map data. (Make sure to run the script through web server)');
122					self.el.removeClass('mapplic-loading').addClass('mapplic-error');
123					alert('Data file missing or invalid!\nMake sure to run the script through web server.');
124				});
125			}
126			else {
127				// inline json object
128				processData(self.o.source);
129				self.el.removeClass('mapplic-loading');
130			}
131
132			return self;
133		}
134
135		// tooltip
136		function Tooltip() {
137			this.el = null;
138			this.pin = null;
139			this.shift = 6;
140			this.drop = 0;
141			this.location = null;
142
143			this.init = function(location, check) {
144				var s = this;
145
146				// markup
147				this.el = $('<div></div>').addClass('mapplic-tooltip');
148				this.wrap = $('<div></div>').addClass('mapplic-tooltip-wrap').appendTo(this.el);
149				this.close = $('<button></button>').append(getIcon('icon-cross')).addClass('mapplic-tooltip-close').attr('aria-label', self.loc.closepopup).on('click touchend', function(e) {
150					e.preventDefault();
151					self.hideLocation();
152					if (!self.o.zoom || self.o.zoomoutclose) self.moveTo(0.5, 0.5, self.fitscale, 400);
153				}).appendTo(this.wrap);
154				this.image = $('<img>').addClass('mapplic-image').attr('alt', 'Location image').hide().appendTo(this.wrap);
155				this.body = $('<div></div>').addClass('mapplic-tooltip-body').attr('aria-modal','true').appendTo(this.wrap);
156				this.title = $('<h4></h4>').addClass('mapplic-tooltip-title').appendTo(this.body);
157				this.content = $('<div></div>').addClass('mapplic-tooltip-content').appendTo(this.body);
158				this.desc = $('<div></div>').addClass('mapplic-tooltip-description').appendTo(this.content);
159				this.link = $('<a>' + self.loc.more + '</a>').addClass('mapplic-popup-link').attr('href', '#').hide().appendTo(this.body);
160				if (self.o.linknewtab) this.link.attr('target', '_blank');
161				this.triangle = $('<div></div>').addClass('mapplic-tooltip-triangle').prependTo(this.wrap);
162
163				// no focus on deeplink
164				if (!check) this.body.attr('tabindex', '-1');
165
166				$('.mapplic-layer.mapplic-visible', self.map).append(this.el);
167
168				if (self.o.smartip) self.el.on('positionchanged', {s: s},  this.repos);
169
170				this.el.css({'transform': 'scale(' + 1/self.scale + ')'});
171
172				if (location) this.show(location);
173
174				// close tooltip
175				$(document).on('keyup.mapplic', function(e) {
176					e.stopImmediatePropagation();
177					if ((e.keyCode === 27)) {
178						self.hideLocation();
179						if (!self.o.zoom || self.o.zoomoutclose) self.moveTo(0.5, 0.5, self.fitscale, 400);
180					}
181				});
182
183				return this;
184			}
185
186			this.repos = function(e) {
187				var s = e.data.s,
188					tx = '-50%',
189					ty = '-100%';
190
191				// vertical
192				var rtop = s.el.offset().top - self.container.el.offset().top;
193				if (rtop < s.wrap.outerHeight() + 36) {
194					s.el.addClass('mapplic-tooltip-bottom');
195					ty = "0%";
196				}
197				else {
198					s.el.removeClass('mapplic-tooltip-bottom');
199					ty = "-100%";
200				}
201			
202				// horizontal
203				var rleft = s.el.offset().left - self.container.el.offset().left;
204				if (rleft < s.wrap.outerWidth() / 2) {
205					if (rleft > 12) tx = -(100 + rleft / s.wrap.outerWidth() * 100) + 100 + "%";
206					else tx = "-10%";
207				}
208				else if (rleft > self.container.el.outerWidth() - s.wrap.outerWidth() / 2) {
209					if (rleft < self.container.el.outerWidth() - 12) tx = (self.container.el.outerWidth() - rleft) / s.wrap.outerWidth() * 100 - 100 + "%";
210					else tx = "-90%";
211				}
212				else tx = "-50%"
213
214				s.wrap.css({ 'transform': 'translate(' + tx + ', ' + ty + ')' });
215			}
216
217			this.show = function(location) {
218				if (location) {
219					var s = this;
220
221					this.el.attr('data-location', location.id);
222
223					this.location = location;
224					if (self.hovertip) self.hovertip.hide();
225
226					if (location.image) {
227						this.image.attr('src', '');
228						this.image.attr('src', location.image).show();
229					}
230					else this.image.hide();
231
232					if (location.link) {
233						this.link.attr('href', location.link).css('background-color', '').show();
234						if (location.color) this.link.css('background-color', location.color);
235					}
236					else this.link.hide();
237
238					this.title.text(location.title);
239					if (location.description) this.desc.html(location.description);
240					else this.desc.empty();
241					this.content[0].scrollTop = 0;
242
243					// shift
244					this.pin = $('.mapplic-pin[data-location="' + location.id + '"]');
245					if (this.pin.length == 0) {
246						this.shift = 20;
247					}
248					else this.shift = Math.abs(parseFloat(this.pin.css('margin-top'))) + 20;
249				
250					// making it visible
251					this.el.stop().css({opacity: 1}).show();
252					setTimeout(function() {s.body.focus() }, 600);
253					this.position();
254					if (self.o.zoom) this.zoom(location);
255
256					// loading & positioning
257					/*
258					$('img', this.el).off('load').on('load', function() {
259						s.position();
260						if (self.o.zoom) s.zoom(location);
261					});*/
262				}
263			}
264
265			this.position = function() {
266				if (this.location) {
267					this.el.css({
268						left: (this.location.x * 100) + '%',
269						top: (this.location.y * 100) + '%'
270					});
271					this.drop = this.el.outerHeight() + this.shift;
272					if (self.o.smartip) this.repos({ data: { s: this}});
273				}
274			}
275
276			this.zoom = function(location) {
277				var ry = 0.5,
278					zoom = location.zoom ? parseFloat(location.zoom)/self.o.maxscale : 1;
279
280				ry = (self.container.el.height()/2 + this.drop/2) / self.container.el.height();
281				self.moveTo(location.x, location.y, zoom, 600, ry);
282			}
283
284			this.hide = function() {
285				var s = this;
286
287				this.location = null;
288				this.el.stop().fadeOut(200, function() { $(this).remove(); });
289				if (self.o.smartip) self.el.off('positionchanged', this.repos);
290			}
291		}
292
293		// hover tooltip
294		function HoverTooltip() {
295			this.el = null;
296			this.pin = null;
297			this.shift = 6;
298
299			this.init = function() {
300				var s = this;
301
302				// construct
303				this.el = $('<div></div>').addClass('mapplic-tooltip mapplic-hovertip');
304				this.wrap = $('<div></div>').addClass('mapplic-tooltip-wrap').appendTo(this.el);
305				this.title = $('<h4></h4>').addClass('mapplic-tooltip-title').appendTo(this.wrap);
306				if (self.o.hovertipdesc) this.desc = $('<div></div>').addClass('mapplic-tooltip-description').appendTo(this.wrap);
307				this.triangle = $('<div></div>').addClass('mapplic-tooltip-triangle').appendTo(this.wrap);
308
309				// events 
310				// markers + old svg
311				$(self.map).on('mouseover', '.mapplic-pin', function() {
312					var id = $(this).data('location');
313
314					s.pin = $('.mapplic-pin[data-location="' + id + '"]');
315					s.shift = Math.abs(parseFloat(s.pin.css('margin-top'))) + 20;
316
317					var location = self.l[id];
318					if (location && location.title) s.show(location);
319				}).on('mouseout', function() {
320					s.hide();
321				});
322
323				// new svg
324				if (self.o.selector) {
325					$(self.map).on('mouseover touchstart', self.o.selector, function() {
326						var location = self.l[$(this).attr('id')];
327						s.shift = 20;
328						if (location && location.title) s.show(location);
329					}).on('mouseout touchend', function() {
330						s.hide();
331					});
332				}
333
334				self.el.on('positionchanged', {s: s},  this.repos);
335
336				self.map.append(this.el);
337
338				return this;
339			}
340
341			this.repos = function(e) {
342				var s = e.data.s,
343					tx = '-50%',
344					ty = '-100%';
345
346				// vertical
347				var rtop = s.el.offset().top - self.container.el.offset().top;
348				if (rtop < s.wrap.outerHeight() + 36) {
349					s.el.addClass('mapplic-tooltip-bottom');
350					ty = "0%";
351				}
352				else {
353					s.el.removeClass('mapplic-tooltip-bottom');
354					ty = "-100%";
355				}
356			
357				// horizontal
358				var rleft = s.el.offset().left - self.container.el.offset().left;
359				if (rleft < s.wrap.outerWidth() / 2) {
360					if (rleft > 12) tx = -(100 + rleft / s.wrap.outerWidth() * 100) + 100 + "%";
361					else tx = "-10%";
362				}
363				else if (rleft > self.container.el.outerWidth() - s.wrap.outerWidth() / 2) {
364					if (rleft < self.container.el.outerWidth() - 12) tx = (self.container.el.outerWidth() - rleft) / s.wrap.outerWidth() * 100 - 100 + "%";
365					else tx = "-90%";
366				}
367				else tx = "-50%"
368
369				s.wrap.css({ 'transform': 'translate(' + tx + ', ' + ty + ')' });
370			}
371
372			this.show = function(location) {
373				if (self.location != location) {
374					this.title.text(location.title);
375					if (self.o.hovertipdesc) {
376						if (location.description) this.desc.html(location.description);
377						else this.desc.empty();
378					}
379					this.position(location);
380
381					this.el.stop().fadeIn(100);
382				}
383				this.repos({ data: { s: this}});
384			}
385
386			this.position = function(location) {
387				if (location) {
388					this.el.css({
389						left: (location.x * 100) + '%',
390						top: (location.y * 100) + '%'
391					});
392
393					this.drop = this.el.outerHeight() + this.shift;
394				}
395			}
396
397			this.hide = function() {
398				this.el.stop().fadeOut(200);
399			}
400		}
401
402		// lightbox
403		function Lightbox() {
404			this.el = null;
405
406			this.init = function() {
407				// construct
408				this.el = $('<div></div>').addClass('mapplic-lightbox mfp-hide');
409				this.title = $('<h4></h4>').addClass('mapplic-lightbox-title').appendTo(this.el);
410				this.desc = $('<div></div>').addClass('mapplic-lightbox-description').appendTo(this.el);
411				this.link = $('<a>' + self.loc.more + '</a>').addClass('mapplic-popup-link').attr('href', '#').hide().appendTo(this.el);
412				if (self.o.linknewtab) this.link.attr('target', '_blank');
413
414				// append
415				self.el.append(this.el);
416
417				return this;
418			}
419
420			this.show = function(location) {
421				this.location = location;
422
423				this.title.text(location.title);
424				this.desc.html(location.description);
425
426				if (location.link) {
427					this.link.attr('href', location.link).css('background-color', '').show();
428					if (location.color) this.link.css('background-color', location.color);
429				}
430				else this.link.hide();
431
432				var s = this;
433
434				$.magnificPopup.open({
435					items: { src: this.el },
436					type: 'inline',
437					removalDelay: 300,
438					mainClass: 'mfp-fade',
439					callbacks: {
440						beforeClose: function() {
441							s.hide();
442						}
443					}
444				});
445
446				// zoom
447				var zoom = location.zoom ? parseFloat(location.zoom) : 1;
448				if (self.o.zoom) self.moveTo(location.x, location.y, zoom, 600);
449
450				return this.el[0];
451			}
452
453			this.showImage = function(location) {
454				this.location = location;
455
456				var s = this;
457
458				$.magnificPopup.open({
459					items: { src: location.image },
460					type: 'image',
461					removalDelay: 300,
462					mainClass: 'mfp-fade',	
463					callbacks: {
464						beforeClose: function() {
465							s.hide();
466						}
467					}
468				});
469
470				// zoom
471				var zoom = location.zoom ? parseFloat(location.zoom) : 1;
472				if (self.o.zoom) self.moveTo(location.x, location.y, zoom, 600);
473			}
474
475			this.hide = function() {
476				this.location = null;
477				this.desc.empty();
478				self.hideLocation();
479				if (!self.o.zoom || self.o.zoomoutclose) self.moveTo(0.5, 0.5, self.fitscale, 400);
480			}
481		}
482
483		// external
484		function External() {
485			this.el = null;
486
487			this.init = function() {
488				this.el = $('<div></div>').addClass('mapplic-external-content').hide();
489				this.title = $('<h4></h4>').addClass('mapplic-external-title').appendTo(this.el);
490				this.desc = $('<div></div>').addClass('mapplic-external-description').appendTo(this.el);
491
492				this.initial = $(self.o.external + ' > *:not(.mapplic-external-content)');
493
494				$(self.o.external).append(this.el);
495				
496				return this;
497			}
498
499			this.show = function(location) {
500				this.title.text(location.title);
501				this.desc.html(location.description);
502
503				this.initial.hide();
504				this.el.show();
505			}
506
507			this.hide = function() {
508				this.initial.show();
509				this.el.hide();
510			}
511		}
512
513		// deeplinking
514		function Deeplinking() {
515			this.param = 'location';
516			this.resolved = false;
517
518			this.init = function() {
519				var s = this;
520
521				window.onpopstate = function(e) {
522					if (e.state) s.check(600);
523					return false;
524				}
525			}
526
527			this.check = function(duration) {
528				var id = this.getUrlParam(this.param);
529				if (id) {
530					self.showLocation(id, duration, true);
531					this.resolved = true;
532				}
533			}
534
535			this.getUrlParam = function(name) {
536				name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
537				var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
538					results = regex.exec(location.search);
539				return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
540			}
541
542			this.update = function(id) {
543				var url;
544				if (typeof window.URL !== 'undefined') {
545					url = new URL(window.location.href);
546					url.searchParams.set(this.param, id);
547					url = url.href
548				} else {
549					url = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + this.param + '=' + id;
550				}
551				window.history.pushState({path: url}, '', url);
552			}
553
554			this.clear = function() {
555				var url;
556				if (typeof window.URL !== 'undefined') {
557					url = new URL(window.location.href);
558					url.searchParams.delete(this.param);
559					url = url.href;
560				} else {
561					url = window.location.pathname;
562				}
563				history.pushState('', document.title, url);
564			}
565		}
566
567		// old hash deeplinking method for old browsers (starting IE9)
568		function DeeplinkingHash() {
569			this.param = 'location';
570			this.resolved = false;
571
572			this.init = function() {
573				var s = this;
574				this.check(0);
575
576				$(window).on('hashchange', function() {
577					//s.check(600);
578				});
579			}
580
581			this.check = function(duration) {
582				var id = location.hash.slice(this.param.length + 2);
583				if (id) {
584					self.showLocation(id, duration, true);
585					this.resolved = true;
586				}
587			}
588
589			this.update = function(id) {
590				window.location.hash = this.param + '-' + id;
591			}
592
593			this.clear = function() {
594				window.location.hash = this.param;
595			}
596		}
597
598		// minimap
599		function Minimap() {
600			this.el = null;
601			this.opacity = null;
602
603			this.init = function() {
604				this.el = $('<div></div>').addClass('mapplic-minimap').appendTo(self.container.el);
605				this.el.click(function(e) {
606					e.preventDefault();
607
608					var x = (e.pageX - $(this).offset().left) / $(this).width(),
609						y = (e.pageY - $(this).offset().top) / $(this).height();
610
611					self.moveTo(x, y, self.scale / self.fitscale, 100);
612				});
613
614				return this;
615			}
616
617			this.addLayer = function(data) {
618				var layer = $('<div></div>').addClass('mapplic-minimap-layer').attr('data-floor', data.id).appendTo(this.el),
619					s = this;
620
621				$('<img>').attr('src', data.minimap).addClass('mapplic-minimap-background').attr('aria-hidden', 'true').appendTo(layer);
622				$('<div></div>').addClass('mapplic-minimap-overlay').appendTo(layer);
623				$('<img>').attr('src', data.minimap).addClass('mapplic-minimap-active').attr('aria-hidden', 'true').on('load', function() {
624					s.update();
625				}).appendTo(layer);
626			}
627
628			this.show = function(target) {
629				$('.mapplic-minimap-layer', this.el).hide();
630				$('.mapplic-minimap-layer[data-floor="' + target + '"]', this.el).show();
631			}
632
633			this.update = function(x, y) {
634				var active = $('.mapplic-minimap-active', this.el);
635
636				if (x === undefined) x = self.x;
637				if (y === undefined) y = self.y;
638
639				var width = (self.container.el.width() / self.contentWidth / self.scale * this.el.width()),
640					height = (self.container.el.height() / self.contentHeight / self.scale * this.el.height()),
641					top = (-y / self.contentHeight / self.scale * this.el.height()),
642					left = (-x / self.contentWidth / self.scale * this.el.width()),
643					right = left + width,
644					bottom = top + height;
645
646				active.each(function() {
647					$(this)[0].style.clip = 'rect(' + top + 'px, ' + right + 'px, ' + bottom + 'px, ' + left + 'px)';
648				});
649
650				// fade out effect
651				var s = this;
652				this.el.show();
653				this.el.css('opacity', 1.0);
654				clearTimeout(this.opacity);
655				this.opacity = setTimeout(function() {
656					s.el.css('opacity', 0);
657					setTimeout(function() { s.el.hide(); }, 600);
658				}, 2000);
659			}
660		}
661
662		// legend
663		function Legend() {
664			this.el = null;
665			this.nr = 0;
666
667			this.init = function() {
668				this.el = $('<div></div>').addClass('mapplic-legend');
669				return this;
670			}
671
672			this.build = function(categories) {
673				var s = this;
674				$.each(categories, function(index, category) {
675					if (category.legend == 'true') s.add(category);
676				});
677				if (this.nr > 0) this.el.appendTo(self.container.el);
678			}
679
680			this.add = function(group) {
681				var toggle = this.newToggle(group, true);
682				if (toggle) toggle.appendTo(this.el);
683				else {
684					var key = $('<span></span>').addClass('mapplic-legend-key');
685					if (group.color) key.css('background-color', group.color);
686					$('<span></span>').addClass('mapplic-legend-label').text(group.title).prepend(key).appendTo(this.el);
687				}
688				this.nr++;
689			}
690
691			this.toggle = function(group, state) {
692				$('*[id="' + group + '"]', self.map).toggle(state);
693				$('*[data-category*="' + group + '"]', self.map).each(function() {
694					var attr = $(this).attr('data-category'); 
695					if (attr == group) $(this).toggle(state);
696					else { // multi-group support
697						var groups = attr.split(','),
698							show = false;
699						groups.forEach(function(g) { if ($('.mapplic-toggle > input[data-group="' + g + '"]')[0] && $('.mapplic-toggle > input[data-group="' + g + '"]')[0].checked) show = true; });
700						$(this).toggle(show);
701					}
702				});
703			}
704
705			this.newToggle = function(group, title) {
706				var s = this,
707					toggle = null;
708
709				if (group.toggle == 'true') {
710					toggle = $('<label class="mapplic-toggle"></label>');
711					var input = $('<input type="checkbox" checked>').attr('data-group', group.id).appendTo(toggle);
712					var circle = $('<span></span>').addClass('mapplic-toggle-circle').appendTo(toggle);
713					if (title) $('<span></span>').addClass('mapplic-legend-label').text(group.title).appendTo(toggle);
714					if (group.switchoff == 'true') input.prop('checked', false);
715					if (group.color) circle.css('background-color', group.color);
716					
717					input.change(function() {
718						$('.mapplic-toggle > input[data-group="' + group.id + '"]', self.el).prop('checked', this.checked);
719						s.toggle(group.id, this.checked);
720					});
721				}
722				return toggle;
723			}
724
725			this.applyToggles = function() {
726				var s = this;
727				$('.mapplic-toggle > input', self.el).each(function() {
728					s.toggle($(this).attr('data-group'), this.checked);
729				});
730			}
731		}
732
733		// groups
734		function Groups() {
735
736			this.init = function() {}
737
738			this.addGroups = function(groups) {
739				if (groups) {
740					$.each(groups, function(index, group) {
741						self.g[group.id] = group;
742						if (group.style && self.s[group.style] && self.s[group.style].base) group.color = self.s[group.style].base.fill; // overwrite color with style
743					});
744				}
745
746				if (self.o.sidebar) self.sidebar.addCategories(groups);
747			}
748		}
749
750		// directory
751		function Directory() {
752			this.dirs = [];
753			this.filters = {};
754
755			this.init = function() {
756				var s = this,
757					event = 'mapready';
758
759				if (self.o.csv) event = 'csvready';
760
761				self.el.on(event, function(e, self) {
762					// dir
763					$('.mapplic-dir:not(.mapplic-dir-results)').each(function() {
764						var attribute = $(this).data('attribute'),
765							pattern = $(this).data('pattern'),
766							title = $(this).text();
767
768						if (title) $('<h3><span>' + title + '</span></h3>').appendTo($(this).empty());
769						s.getDirectory(attribute, pattern, attribute).appendTo($(this));
770
771					});
772
773					// filter
774					$('.mapplic-dir-filter').each(function() {
775						var el = $(this),
776							attribute = el.data('attribute'),
777							options = [];
778
779						// generate options
780						if ($(this).hasClass('mapplic-dir-filter-generate')) {
781							$.each(self.l, function(id, location) {
782								if (location[attribute]) {
783									var atts = location[attribute].toString().split(',');
784									atts.forEach(function(att) { if (options.indexOf(att) === -1) options.push(att); });
785								}
786							});
787
788							options.forEach(function(option) {
789								var text = option;
790
791								if (attribute == 'category' && self.g[option]) text = self.g[option].title;
792								$('<option></option>').attr('value', option).text(text).appendTo(el);
793							});
794						}
795
796						// changed
797						$(this).on('change', function() {
798							if (this.value) s.filters[attribute] = this.value;
799							else delete s.filters[attribute];
800
801							s.search();
802						});
803					});
804				});
805
806
807				// search
808				$('.mapplic-dir-search').keyup(function(e) {
809					var val = $(this).val();
810
811					if (val) s.filters['keyword'] = val;
812					else delete s.filters['keyword'];
813
814					s.search();
815
816					if (e.keyCode == 13) $('.mapplic-dir-results li > a').filter(':first').click();
817					else if (e.keyCode == 27) s.clearFilters();
818				});
819
820				// results
821				var restitle = $('<h3><span>0</span> found</h3>').appendTo($('.mapplic-dir-results'));
822				$('<ul></ul>').appendTo($('.mapplic-dir-results'));
823				$('<button></button>').text('Clear').append(getIcon('icon-cross')).addClass('mapplic-dir-results-clear').appendTo(restitle).on('click', function() {
824					s.clearFilters();
825				});
826
827				// list/grid
828				var directories = $('.mapplic-dir-view').parent().find('.mapplic-dir');
829				$('.mapplic-dir-view-list').click(function() {
830					$(this).hide();
831					$('.mapplic-dir-view-grid').show();
832					directories.removeClass('mapplic-dir-grid');
833				});
834				$('.mapplic-dir-view-grid').click(function() {
835					$(this).hide();
836					$('.mapplic-dir-view-list').show();
837					directories.addClass('mapplic-dir-grid');
838				});
839
840				return this;
841			}
842
843			this.getDirectory = function(attribute, pattern, sortby) {
844				var s = this,
845					dir = $('<ul></ul>'),
846					regex = new RegExp(pattern, 'i');
847				$.each(self.l, function(id, location) {
848					if (attribute && pattern && !regex.test(location[attribute])) return true; // skip if no match
849					if (String(location.hide) != 'true') dir.append(s.getItem(location));
850				});
851
852				if (sortby) this.sortby(dir, sortby);
853
854				this.dirs.push(dir);
855
856				// trigger event
857				self.el.trigger('dirready', dir);
858
859				return dir;
860			}
861
862			this.getItem = function(location) {
863				if (!location.id) location = self.l[location];
864
865				var item = $('<li></li>').addClass('mapplic-dir-item').attr('data-location', location.id);
866				var link = $('<a></a>').attr('href', '#').click(function(e) {
867					e.preventDefault();
868					self.showLocation(location.id, 600);
869
870					// scroll back to map on mobile
871					if (($(window).width() < 668) && (location.action || self.o.action) != 'lightbox') {
872						$('html, body').animate({
873							scrollTop: self.container.el.offset().top
874						}, 400);
875					}
876				}).appendTo(item);
877
878				if (self.o.sortby) item.data('sort', location[self.o.sortby]);
879
880				if (self.o.thumbholder) {
881					var thumbnail = this.thumbnail(location.title, location.thumbnail);
882					if (thumbnail) thumbnail.appendTo(link);
883				}
884
885				$('<h4></h4>').text(location.title).appendTo(link);
886				$('<span></span>').html(location.about).addClass('mapplic-about').appendTo(link);
887				if (location.color) item.css('border-color', location.color);
888
889				location.list = item;
890
891				return item;
892			}
893
894			this.thumbnail = function(name, field) {
895				var elem = null;
896
897				if (field) {
898					if (field.match(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g)) elem = $('<img>').attr('src', field).attr('alt', name).addClass('mapplic-thumbnail');
899					else elem = $('<div></div>').addClass('mapplic-thumbnail mapplic-thumbnail-placeholder').attr('aria-hidden', 'true').html(field);
900				}
901				else if (name) {
902					var words = name.split(' '),
903						text = '';
904
905					if (words[0]) text += words[0][0];
906					if (words[1]) text += words[1][0];
907					elem = $('<div></div>').addClass('mapplic-thumbnail mapplic-thumbnail-placeholder').attr('aria-hidden', 'true').text(text.toUpperCase());
908				}
909
910				return elem;
911			}
912
913			this.search = function() {
914				var s = this;
915
916				$('.mapplic-dir-results ul').empty();
917
918				if (self.o.highlight) {
919					self.map.removeClass('mapplic-filtered');
920					$('.mapplic-highlight', self.map).removeClass('mapplic-highlight');
921				}
922
923				$.each(self.l, function(i, location) {
924					var matched = true; // matched false, current ||
925
926					$.each(s.filters, function(attribute, filter) {
927						var current = false;
928
929						if (attribute == 'keyword') $.each(self.o.searchfields, function(i, field) { if (location[field] && !current) current = !(s.normalizeString(location[field]).indexOf(s.normalizeString(filter)) == -1); });
930						else if (location[attribute] && location[attribute].indexOf(filter) != -1) current = true;
931
932						matched = matched && current;
933					});
934
935					if (matched) {
936						$('.mapplic-dir-item[data-location="' + location.id + '"]').clone(true).appendTo($('.mapplic-dir-results ul')); // results
937						if (self.o.highlight && !$.isEmptyObject(s.filters) && location.el) location.el.addClass('mapplic-highlight'); // highlight
938					}
939				});
940
941				s.sortby($('.mapplic-dir-results ul'), 'title'); // alphabetic sort by title
942
943				if ($.isEmptyObject(s.filters)) {
944					$('.mapplic-dir').show();
945					$('.mapplic-dir-results').hide();
946				}
947				else {
948					$('.mapplic-dir').hide();
949					$('.mapplic-dir-results').show();
950					if (self.o.highlight) self.map.addClass('mapplic-filtered');
951				}
952
953				$('.mapplic-dir-results h3 span').text($('.mapplic-dir-results .mapplic-dir-item').length); // count
954			}
955
956			this.clearFilters = function() {
957				this.filters = {};
958				$('.mapplic-dir-search').val('');
959				$('.mapplic-dir-filter').val('');
960
961				this.search();
962			}
963
964			this.normalizeString = function(s) {
965				if (s) return s.toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, "");
966				else return '';
967			}
968
969			this.sort = function(dir, order) {
970				if (order === undefined) order = 1;
971				var items = dir.children('.mapplic-dir-item');
972
973				items.sort(function(a, b) {
974					var a = $(a).data('sort'),
975						b = $(b).data('sort');
976					if (isNaN(a) || isNaN(b)) {
977						return (a < b) ? -1 * order : 1 * order;
978					}
979					else return (a - b) * order;
980				}).appendTo(dir);
981			}
982
983			this.sortby = function(dir, attribute, order) {
984				var s = this;
985
986				if (order === undefined) order = 1;
987				var items = dir.children('.mapplic-dir-item');
988				items.each(function() {
989					$(this).data('sort', s.normalizeString(self.l[$(this).data('location')][attribute]));
990				});
991				this.sort(dir, order);
992			}			
993		}
994
995		// sidebar
996		function Sidebar() {
997			this.el = null;
998			this.header = null;
999			this.clear = null;
1000			this.input = null;
1001			this.list = null;
1002			this.tags = null;
1003			this.toggle = null;
1004
1005			this.init = function() {
1006				var s = this;
1007
1008				this.el = $('<div></div>').addClass('mapplic-sidebar').appendTo(self.el);
1009
1010				if (self.o.filtersopened) this.el.addClass('mapplic-sidebar-header-opened');
1011				
1012				if (self.o.sidebartoggle) {
1013					self.el.addClass('mapplic-dynamic-sidebar');
1014
1015					this.toggle = $('<button></button>').append(getIcon('icon-sidebar')).addClass('mapplic-button mapplic-sidebar-toggle').appendTo(self.container.el);
1016					this.toggle.on('click touchstart', function(e) {
1017						e.preventDefault();
1018						self.el.toggleClass('mapplic-hidden-sidebar');
1019						setTimeout(function() {
1020							var wr = self.container.el.width() / self.contentWidth,
1021								hr = self.container.el.height() / self.contentHeight;
1022
1023							if (self.o.mapfill) {
1024								if (wr > hr) self.fitscale = wr;
1025								else self.fitscale = hr;
1026							}
1027							else {
1028								if (wr < hr) self.fitscale = wr;
1029								else self.fitscale = hr;
1030							}
1031
1032							if (self.container.oldW != self.container.el.width() || self.container.oldH != self.container.el.height()) {
1033
1034								self.container.oldW = self.container.el.width();
1035								self.container.oldH = self.container.el.height();
1036
1037								if (self.scale*self.contentWidth < self.container.el.width()) self.moveTo(self.x, self.y, self.scale, 600);
1038							}
1039
1040						}, 400);
1041					});
1042				}
1043
1044				if (self.o.search) {
1045					this.header = $('<div></div>').addClass('mapplic-sidebar-header').append(getIcon('icon-magnifier')).appendTo(this.el);
1046					this.clear = $('<button></button>').addClass('mapplic-search-clear').append(getIcon('icon-cross')).appendTo(this.header);
1047					this.headerwrap = $('<div></div>').appendTo(this.header);
1048
1049					this.input = $('<input>').attr({'type': 'text', 'spellcheck': 'false', 'placeholder': self.loc.search}).addClass('mapplic-search-input').keyup(function(e) {
1050						var val = $(this).val();
1051
1052						if (val) self.directory.filters['keyword'] = val;
1053						else delete self.directory.filters['keyword'];
1054
1055						s.el.toggleClass('mapplic-sidebar-header-opened', val.length < 1);
1056
1057						s.search();
1058
1059						if (e.keyCode == 13) $('.mapplic-list-container li > a').filter(':visible:first').click();
1060						else if (e.keyCode == 27) $(this).blur();
1061					});
1062					this.input.focus(function() {
1063						if ($(this).val().length < 1) s.el.addClass('mapplic-sidebar-header-opened');
1064					});
1065
1066					$('body').click(function(e) {
1067						if (!$.contains(s.header[0], e.target) && s.header[0] != e.target) s.el.removeClass('mapplic-sidebar-header-opened');
1068					});
1069
1070					this.input.appendTo(this.headerwrap);
1071
1072					// clear search
1073					this.clear.on('click touchstart', function(e) {
1074						e.preventDefault();
1075						s.input.val('');
1076						s.tags.empty();
1077						if (s.tags.children().length < 1) s.el.removeClass('mapplic-sidebar-tagsrow');
1078						self.directory.filters = {};
1079
1080						s.search();
1081					});
1082
1083					this.toggle = $('<button></button>').append(getIcon('icon-filter')).addClass('mapplic-search-toggle').click(function(e) {
1084						e.preventDefault();
1085						s.el.toggleClass('mapplic-sidebar-header-opened');
1086					}).appendTo(this.headerwrap);
1087
1088					// tags
1089					this.tags = $('<div></div>').addClass('mapplic-filter-tags').appendTo(this.headerwrap);
1090
1091					// filters
1092					this.filter = $('<div></div>').addClass('mapplic-filter').appendTo(this.header);
1093
1094					// dim
1095					this.dim = $('<div></div>').addClass('mapplic-sidebar-dim').click(function() { s.el.removeClass('mapplic-sidebar-header-opened'); }).appendTo(this.el);
1096				}
1097				else this.el.addClass('mapplic-sidebar-nosearch');
1098
1099				// items
1100				var event = 'mapready';
1101				if (self.o.csv) event = 'csvready';
1102
1103				self.el.on(event, function(e, self) {
1104					s.dir = self.directory.getDirectory().addClass('mapplic-list-container').appendTo(s.el);
1105
1106					// alphabetic sort by title
1107					if (self.o.alphabetic) self.directory.sortby(s.dir, 'title');
1108				});
1109
1110				if (self.o.searchdescription && self.o.searchfields.indexOf('description') === -1) self.o.searchfields.push('description');
1111			}
1112
1113			this.addTag = function(item) {
1114				var s = this,
1115					categories = [];
1116
1117				if (self.directory.filters['category']) categories = self.directory.filters['category'].split(',');
1118
1119				if (!categories.includes(item.id)) {
1120					categories.push(item.id);
1121					self.directory.filters['category'] = categories.toString();
1122
1123					this.el.addClass('mapplic-sidebar-tagsrow');
1124
1125					var tag = $('<button></button>').addClass('mapplic-tag').text(item.title).attr('aria-label', self.loc.clearfilter + ': ' + item.title).prependTo(this.tags);
1126					$('<span></span>').appendTo(tag);
1127					if (item.color) tag.css('background-color', item.color);
1128					if (item.id) tag.attr('data-id', item.id);
1129
1130					tag.click(function() {
1131						tag.remove();
1132						if (s.tags.children().length < 1) s.el.removeClass('mapplic-sidebar-tagsrow');
1133						if (self.directory.filters['category'] == item.id) delete self.directory.filters['category'];
1134						else if (self.directory.filters['category']) {
1135							categories = self.directory.filters['category'].split(',');
1136							var i = categories.indexOf(item.id);
1137							if (i > -1) {
1138								categories.splice(i, 1)
1139								self.directory.filters['category'] = categories.toString();
1140							}
1141						}
1142						s.search();
1143					}).appendTo(tag);
1144
1145					s.search();
1146					this.el.removeClass('mapplic-sidebar-header-opened');
1147				}
1148			}
1149
1150			this.addCategories = function(categories) {
1151				var s = this,
1152					list = $('<ul></ul>'),
1153					add = false;
1154
1155				if (categories) {
1156					$.each(categories, function(index, category) {
1157						category.nr = 0;
1158
1159						if (!(category.hide == 'true')) {
1160							var item = $('<li></li>').addClass('mapplic-list-category').attr('data-category', category.id);
1161							var link = $('<a></a>').attr('href', '#').prependTo(item);
1162
1163							var thumbnail = self.directory.thumbnail(category.title, category.icon).appendTo(l
1163ink);
1164							if (category.color && thumbnail) thumbnail.css({'background-color': category.color, 'border-color': category.color });
1165
1166							var title = $('<h4></h4').text(category.title).appendTo(link);
1167							if (!category.about) title.addClass('mapplic-margin');
1168							else $('<span></span>').addClass('mapplic-about').html(category.about).appendTo(link);
1169							category.count = $('<span></span>').text('(0)').addClass('mapplic-list-count').appendTo(title);
1170							
1171							var toggle = self.legend.newToggle(category)
1172							if (toggle) toggle.appendTo(item);
1173
1174							
1175							link.on('click', function(e) {
1176								e.preventDefault();
1177								if (category.nr < 1 && toggle) $('> input', toggle).trigger('click');
1178								else {
1179									//s.input.val('');
1180									s.addTag(category);
1181								}
1182							});
1183
1184							category.list = item;
1185							item.appendTo(list);
1186							add = true;
1187						}
1188					});
1189
1190					if (add) {
1191						s.el.addClass('mapplic-sidebar-filterable');
1192						
1193						//$('<h5></h5>').text('CATEGORIES').appendTo(s.filter);
1194						
1195						list.appendTo(s.filter);
1196					}
1197				}
1198			}
1199
1200			this.countCategory = function() {
1201				$.each(self.g, function(i, group) {
1202					if (group.count) {
1203						group.count.text('(' + group.nr + ')');
1204						if (group.nr < 1) group.count.hide();
1205						else group.count.show();
1206					}
1207				});
1208			}
1209
1210			// search
1211			this.search = function() {
1212				var s = this;
1213
1214				if (self.o.highlight) {
1215					self.map.removeClass('mapplic-filtered');
1216					$('.mapplic-highlight', self.map).removeClass('mapplic-highlight');
1217				}
1218
1219				$.each(self.l, function(i, location) {
1220					var matched = true; // matched false, current ||
1221
1222					$.each(self.directory.filters, function(attribute, filter) {
1223						var current = false;
1224
1225						if (attribute == 'keyword') $.each(self.o.searchfields, function(i, field) { if (location[field] && !current) current = !(self.directory.normalizeString(location[field]).indexOf(self.directory.normalizeString(filter)) == -1); });
1226						else if (location.category && attribute == 'category') {
1227							var categories = location.category;
1228							if (typeof categories == 'string') categories = categories.split(',');
1229
1230							categories.forEach(function(category) {
1231								if (self.directory.filters['category'].split(',').includes(category)) current = true;
1232							});
1233						}
1234						else if (location[attribute] && location[attribute].indexOf(filter) != -1) current = true;
1235
1236						matched = matched && current;
1237					});
1238
1239					var item = $('.mapplic-dir-item[data-location="' + location.id + '"]', self.el);
1240					if (matched) {
1241						item.show();
1242						if (self.o.highlight && !$.isEmptyObject(self.directory.filters) && location.el) location.el.addClass('mapplic-highlight'); // highlight
1243					}
1244					else item.hide();
1245				});
1246
1247				if (!$.isEmptyObject(self.directory.filters)) {
1248					if (self.o.highlight) self.map.addClass('mapplic-filtered');
1249					self.el.addClass('mapplic-search-active');
1250				}
1251				else self.el.removeClass('mapplic-search-active');
1252			}
1253		}
1254
1255		// developer tools
1256		function DevTools() {
1257			this.el = null;
1258
1259			this.init = function() {
1260				this.el = $('<div></div>').addClass('mapplic-coordinates').appendTo(self.container.el);
1261				this.el.append('x: ');
1262				$('<code></code>').addClass('mapplic-coordinates-x').appendTo(this.el);
1263				this.el.append(' y: ');
1264				$('<code></code>').addClass('mapplic-coordinates-y').appendTo(this.el);
1265
1266				$('.mapplic-layer', self.map).on('mousemove', function(e) {
1267					var x = (e.pageX - self.map.offset().left) / self.map.width() / self.scale,
1268						y = (e.pageY - self.map.offset().top) / self.map.height() / self.scale;
1269					$('.mapplic-coordinates-x').text(parseFloat(x).toFixed(4));
1270					$('.mapplic-coordinates-y').text(parseFloat(y).toFixed(4));
1271				});
1272
1273				return this;
1274			}
1275		}
1276
1277		// clear button
1278		function ClearButton() {
1279			this.el = null;
1280			
1281			this.init = function() {
1282				this.el = $('<button></button>').text(self.loc.resetzoom).append(getIcon('icon-reset')).addClass('mapplic-button mapplic-clear-button').appendTo(self.container.el);
1283
1284				if (!self.o.zoombuttons) this.el.css('bottom', '0');
1285
1286				this.el.on('click touchstart', function(e) {
1287					e.preventDefault();
1288					self.hideLocation();
1289					self.moveTo(0.5, 0.5, self.fitscale, 400);
1290				});
1291
1292				return this;
1293			}
1294
1295			this.update = function(scale) {
1296				if (scale == self.fitscale) this.el.stop().fadeOut(200);
1297				else this.el.stop().fadeIn(200);
1298			}
1299		}
1300
1301		// zoom buttons
1302		function ZoomButtons() {
1303			this.el = null;
1304		
1305			this.init = function() {
1306				this.el = $('<div></div>').addClass('mapplic-zoom-buttons').appendTo(self.container.el);
1307
1308				// zoom-in button
1309				this.zoomin = $('<button></button>').attr('aria-label', self.loc.zoomin).append(getIcon('icon-plus')).addClass('mapplic-button mapplic-zoomin-button').appendTo(this.el);
1310				this.zoomin.on('click touchstart', function(e) {
1311					e.preventDefault();
1312					self.container.stopMomentum();
1313
1314					var scale = self.scale;
1315					self.scale = normalizeScale(scale + scale * 0.8);
1316
1317					self.x = normalizeX(self.x - (self.container.el.width() / 2 - self.x) * (self.scale / scale - 1));
1318					self.y = normalizeY(self.y - (self.container.el.height() / 2 - self.y) * (self.scale / scale - 1));
1319
1320					zoomTo(self.x, self.y, self.scale, 400, 'ease');
1321				});
1322
1323				// zoom-out button
1324				this.zoomout = $('<button></button>').attr('aria-label', self.loc.zoomout).append(getIcon('icon-minus')).addClass('mapplic-button mapplic-zoomout-button').appendTo(this.el);
1325				this.zoomout.on('click touchstart', function(e) {
1326					e.preventDefault();
1327					self.container.stopMomentum();
1328
1329					var scale = self.scale;
1330					self.scale = normalizeScale(scale - scale * 0.4);
1331
1332					self.x = normalizeX(self.x - (self.container.el.width() / 2 - self.x) * (self.scale / scale - 1));
1333					self.y = normalizeY(self.y - (self.container.el.height() / 2 - self.y) * (self.scale / scale - 1));
1334
1335					zoomTo(self.x, self.y, self.scale, 400, 'ease');
1336				});
1337
1338				return this;
1339			}
1340
1341			this.update = function(scale) {
1342				this.zoomin.removeAttr('disabled');
1343				this.zoomout.removeAttr('disabled');
1344				if (scale == self.fitscale) this.zoomout.attr('disabled','disabled');
1345				else if (scale == 1) this.zoomin.attr('disabled','disabled');
1346			}
1347		}
1348
1349		// fullscreen
1350		function Fullscreen() {
1351			this.el = null;
1352			this.fsh = null; // fullscreen placeholder
1353			this.esh = null; // element placeholder
1354
1355			this.init = function() {
1356				var s = this;
1357
1358				this.fph = $('<div></div>').addClass('mapplic-fsh').prependTo('body');
1359				this.eph = $('<div></div>').addClass('mapplic-esh').insertBefore(self.el);
1360
1361				// fullscreen button
1362				this.el = $('<button></button>').append(getIcon('icon-fullscreen')).append(getIcon('icon-fullscreen-exit')).addClass('mapplic-button mapplic-fullscreen-button').click(function(e) {
1363					self.el.toggleClass('mapplic-fullscreen');
1364
1365					if (self.el.hasClass('mapplic-fullscreen')) self.el.insertBefore(s.fph);
1366					else self.el.insertBefore(s.eph);
1367
1368					$(document).resize();
1369				}).appendTo(self.container.el);
1370
1371				// esc key
1372				$(document).on('keyup.mapplic', function(e) {
1373					if ((e.keyCode === 27) && $('.mapplic-fullscreen')[0]) {
1374						$('.mapplic-element.mapplic-fullscreen').removeClass('mapplic-fullscreen');
1375						self.el.insertBefore(s.eph);
1376						$(document).resize();
1377					}
1378				});
1379			}
1380		}
1381
1382		// styles
1383		function Styles() {
1384			this.data = null;
1385
1386			this.init = function(data) {
1387				this.data = data;
1388				this.process(data);
1389				return this;
1390			}
1391
1392			this.process = function(styles) {
1393				var css = '';
1394
1395				// basecolor
1396				if (self.o.basecolor) css += this.rule('.mapplic-fullscreen, .mapplic-legend', 'background-color', self.o.basecolor);
1397
1398				// bgcolor
1399				if (self.o.bgcolor) {
1400					css += this.rule(
1401						'.mapplic-button, .mapplic-tooltip-close .mapplic-icon, .mapplic-levels-select, .mapplic-levels button, .mapplic-level-switcher button.mapplic-selected, .mapplic-element *::-webkit-scrollbar-track, .mapplic-list-container, .mapplic-filter, .mapplic-sidebar-header, .mapplic-tooltip-wrap, .mapplic-lightbox, .mapplic-toggle:before',
1402						'background-color',
1403						self.o.bgcolor
1404					);
1405					css += this.rule('.mapplic-legend-key, .mapplic-element *::-webkit-scrollbar-thumb', 'border-color', self.o.bgcolor);
1406					css += this.rule('.mapplic-tooltip:after', 'border-color', self.o.bgcolor + ' transparent transparent transparent !important');
1407					css += this.rule('.mapplic-tooltip-bottom.mapplic-tooltip:after', 'border-color', 'transparent transparent ' + self.o.bgcolor + ' transparent !important');
1408				}
1409
1410				// bgcolor2
1411				if (self.o.bgcolor2) {
1412					css += this.rule(
1413						'.mapplic-thumbnail-placeholder, .mapplic-level-switcher button, .mapplic-sidebar .mapplic-dir-item > a:hover, .mapplic-sidebar-header-opened.mapplic-sidebar-filterable .mapplic-search-toggle, .mapplic-sidebar .mapplic-dir-item > a:focus, .mapplic-sidebar .mapplic-dir-item.mapplic-active > a, .mapplic-list-category > a:hover, .mapplic-zoom-buttons button:disabled, .mapplic-levels button:disabled',
1414						'background-color',
1415						self.o.bgcolor2
1416					);
1417					css += this.rule('a.mapplic-zoomin-button', 'border-color', self.o.bgcolor2);
1418				}
1419
1420				// headingcolor
1421				if (self.o.headingcolor) {
1422					css += this.rule('.mapplic-search-input, .mapplic-level-switcher button.mapplic-selected, .mapplic-list-category > a, .mapplic-tooltip-title, .mapplic-lightbox-title, .mapplic-sidebar .mapplic-dir-item h4, .mapplic-element strong, .mapplic-levels-select, .mapplic-list-category h4', 'color', self.o.headingcolor);
1423					css += this.rule('.mapplic-icon', 'fill', self.o.headingcolor);
1424				}
1425
1426				// textcolor
1427				if (self.o.textcolor) css += this.rule('.mapplic-element, .mapplic-element a, .mapplic-level-switcher button, .mapplic-about, .mapplic-list-category > a .mapplic-list-count, .mapplic-search-input::placeholder, .mapplic-lightbox-description', 'color', self.o.textcolor);
1428
1429				// accentcolor
1430				if (self.o.accentcolor) css += this.rule('.mapplic-popup-link, .mapplic-accentcolor', 'background-color', self.o.accentcolor);
1431
1432				if (styles) {
1433					self.s = [];
1434
1435					styles.forEach(function(s) {
1436						// styles object
1437						self.s[s.class] = s;
1438
1439						if (s.base) {
1440							css += '.' + s.class + '.mapplic-clickable:not(g), g.' + s.class + '.mapplic-clickable > * { ';
1441							$.each(s.base, function(prop, val) { css += prop + ': ' + val + '; '; });
1442							css += '}\n';
1443
1444							css += '.' + s.class + '.mapplic-pin {\n';
1445							$.each(s.base, function(prop, val) {
1446								css += '	background-color: ' + val + ';\n';
1447								css += '	border-color: ' + val + ';\n';
1448							});
1449							css += '}\n\n';
1450						}
1451
1452						if (s.hover) {
1453							css += '.' + s.class + '.mapplic-highlight:not(g), g.' + s.class + '.mapplic-highlight > *, .' + s.class + '.mapplic-clickable:not(g):hover, g.' + s.class + '.mapplic-clickable:hover > * { ';
1454							$.each(s.hover, function(prop, val) { css += prop + ': ' + val + '; ' });
1455							css += '}\n';
1456
1457							css += '.' + s.class + '.mapplic-pin.mapplic-highlight, .' + s.class + '.mapplic-pin:hover {\n';
1458							$.each(s.hover, function(prop, val) {
1459								css += '	background-color: ' + val + ';\n';
1460								css += '	border-color: ' + val + ';\n';
1461							});
1462							css += '}\n\n';
1463						}
1464
1465						if (s.active) {
1466							css += '.'+ s.class + '.mapplic-active:not(g), g.' + s.class + '.mapplic-active > * { ';
1467							$.each(s.active, function(prop, val) { css += prop + ': ' + val + ' !important; ' });
1468							css += '}\n';
1469
1470							css += '.' + s.class + '.mapplic-pin.mapplic-active {\n';
1471							$.each(s.active, function(prop, val) {
1472								css += '	background-color: ' + val + ';\n';
1473								css += '	border-color: ' + val + ';\n';
1474							});
1475							css += '}\n\n';
1476						}
1477					});
1478				}
1479
1480				if (self.o.customcss) css += self.o.customcss;
1481				
1482				var style = $('<style></style>').html(css).appendTo('body');
1483			}
1484
1485			this.rule = function(selector, attribute, value) {
1486				var css = selector + ' {\n';
1487				css += '	' + attribute + ': ' + value + ';\n';
1488				css += '}\n\n';
1489
1490				return css;
1491			}
1492		}
1493
1494		function Container() {
1495			this.el = null;
1496			this.oldW = 0;
1497			this.oldH = 0;
1498			this.position = {x: 0, y: 0},
1499			this.momentum = null;
1500			this.levels = {};
1501
1502			this.init = function() {
1503				this.el = $('<div></div>').addClass('mapplic-container').appendTo(self.el);
1504				self.map = $('<div></div>').addClass('mapplic-map').appendTo(this.el);
1505
1506				self.map.css({
1507					'width': self.contentWidth,
1508					'height': self.contentHeight
1509				});
1510
1511				return this;
1512			}
1513
1514			// returns container height (px)
1515			this.calcHeight = function(v) {
1516				var val = Math.min(Math.max(this.getHeight(v), this.getHeight(self.o.minheight)), this.getHeight(self.o.maxheight));
1517
1518				if ($.isNumeric(val)) return val;
1519				else return false;
1520			}
1521
1522			this.getHeight = function(v) {
1523				var val = v.toString().replace('px', '');
1524
1525				if ((val == 'auto') && (self.container.el))  val = self.container.el.width() * self.contentHeight / self.contentWidth; 
1526				else if (val.slice(-1) == '%') val = $(window).height() * val.replace('%', '') / 100;
1527
1528				return val;
1529			}
1530
1531			this.resetZoom = function() {
1532				var init = self.l['init'];
1533				if (init) {
1534					self.switchLevel(init.level);
1535					if (!self.bboxZoom(init.el)) self.zoomLocation(init);
1536				}
1537				else self.moveTo(0.5, 0.5, self.fitscale, 0);
1538			}
1539
1540			this.revealChild = function(parent) {
1541				$('.mapplic-pin[data-location^=' + parent.id + '-]', self.map).addClass('mapplic-revealed');
1542				$('.mapplic-map-image [id^=' + parent.id + '-]', self.map).addClass('mapplic-revealed');
1543			}
1544
1545			this.revealZoom = function(zoom) {
1546				$('.mapplic-pin[data-reveal]', self.map).each(function() {
1547					var reveal = $(this).data('reveal');
1548					if (zoom * self.o.maxscale >= reveal) $(this).addClass('mapplic-revealed');
1549					else $(this).removeClass('mapplic-revealed');
1550				});
1551			}
1552
1553			this.coverAll = function() {
1554				$('.mapplic-revealed', self.map).removeClass('mapplic-revealed');
1555			}
1556
1557			this.stopMomentum = function() {
1558				cancelAnimationFrame(this.momentum);
1559				if (this.momentum != null) {
1560					self.x = this.position.x;
1561					self.y = this.position.y;
1562				}
1563				this.momentum = null;
1564			}
1565
1566			this.addLevelSwitcher = function() {
1567				if (self.data.levels.length > 1) {
1568					var control = $('<div></div>').addClass('mapplic-level-switcher');
1569					self.data.levels.forEach(function(level, i) {
1570						var button = $('<button></button>').attr('data-level', level.id).text(level.title).prependTo(control).click(function(e) {
1571							e.preventDefault();
1572							self.switchLevel(level.id);
1573						});
1574						if (level.show) button.addClass('mapplic-selected');
1575					});
1576
1577					this.el.append(control);
1578
1579					self.el.on('levelswitched', function(e, target) {
1580						$('button', control).removeClass('mapplic-selected');
1581						$('button[data-level="' +  target + '"]', control).addClass('mapplic-selected');
1582					});
1583				}
1584			}
1585
1586			this.addControls = function() {
1587				self.map.addClass('mapplic-zoomable');
1588
1589				document.ondragstart = function() { return false; } // IE drag fix
1590
1591				// momentum
1592				var friction = 0.85,
1593					mouse = {x: 0, y: 0},
1594					pre = {x: 0, y: 0},
1595					previous = {x: this.position.x, y: this.position.y},
1596					velocity = {x: 0, y: 0};
1597
1598				var s = this;
1599				var momentumStep = function() {
1600					s.momentum = requestAnimationFrame(momentumStep);
1601
1602					if (self.map.hasClass('mapplic-dragging')) {
1603						pre.x = previous.x;
1604						pre.y = previous.y;
1605
1606						previous.x = s.position.x;
1607						previous.y = s.position.y;
1608
1609						s.position.x = mouse.x;
1610						s.position.y = mouse.y;
1611						
1612						velocity.x = previous.x - pre.x;
1613						velocity.y = previous.y - pre.y;
1614					}
1615					else {
1616						s.position.x += velocity.x;
1617						s.position.y += velocity.y;
1618						
1619						velocity.x *= friction;
1620						velocity.y *= friction;
1621
1622						if (Math.abs(velocity.x) + Math.abs(velocity.y) < 0.1) {
1623							s.stopMomentum();
1624							self.x = s.position.x;
1625							self.y = s.position.y;
1626						}
1627					}
1628					s.position.x = normalizeX(s.position.x);
1629					s.position.y = normalizeY(s.position.y);
1630
1631					zoomTo(s.position.x, s.position.y);
1632				}
1633
1634				// drag & drop
1635				$('.mapplic-map-image', self.map).on('mousedown', function(e) {
1636					self.dragging = false;
1637					self.map.addClass('mapplic-dragging');
1638					
1639					s.stopMomentum();
1640					var initial = {
1641						x: e.pageX - self.x,
1642						y: e.pageY - self.y
1643					};
1644
1645					mouse.x = normalizeX(e.pageX - initial.x);
1646					mouse.y = normalizeY(e.pageY - initial.y);
1647					momentumStep();
1648
1649					self.map.on('mousemove', function(e) {
1650						self.dragging = true;
1651
1652						mouse.x = normalizeX(e.pageX - initial.x);
1653						mouse.y = normalizeY(e.pageY - initial.y);
1654					});
1655				
1656					$(document).on('mouseup.mapplic', function() {
1657						$(document).off('mouseup.mapplic');
1658						self.map.off('mousemove');
1659						self.map.removeClass('mapplic-dragging');
1660					});
1661				});
1662
1663				// mousewheel
1664				if (self.o.mousewheel) $('.mapplic-map-image', self.el).on('mousewheel DOMMouseScroll', self.mouseWheel);
1665
1666				// touch
1667				var init1 = null,
1668					init2 = null,
1669					initD = 0,
1670					initScale = null;
1671
1672				$('.mapplic-map-image', self.map).on('touchstart', function(e) {
1673					e.preventDefault();
1674					var eo = e.originalEvent;
1675
1676					if (eo.touches.length == 1) {
1677						self.map.addClass('mapplic-dragging');
1678						self.dragging = false;
1679
1680						s.stopMomentum();
1681						
1682						init1 = {
1683							x: eo.touches[0].pageX - self.x,
1684							y: eo.touches[0].pageY - self.y
1685						};
1686
1687						self.firstcoord = { x: eo.touches[0].pageX, y: eo.touches[0].pageY };
1688
1689						mouse = {
1690							x: normalizeX(eo.touches[0].pageX - init1.x),
1691							y: normalizeY(eo.touches[0].pageY - init1.y)
1692						};
1693						momentumStep();
1694
1695						self.map.on('touchmove', function(e) {
1696							e.preventDefault();
1697							self.dragging = true;
1698							var eo = e.originalEvent;
1699
1700							if (eo.touches.length == 1) {
1701								mouse.x = normalizeX(eo.touches[0].pageX - init1.x);
1702								mouse.y = normalizeY(eo.touches[0].pageY - init1.y);
1703
1704								self.lastcoord = { x: eo.touches[0].pageX, y: eo.touches[0].pageY };
1705							}
1706							else if (eo.touches.length > 1) {
1707								var pos = {
1708									x: (eo.touches[0].pageX + eo.touches[1].pageX)/2,
1709									y: (eo.touches[0].pageY + eo.touches[1].pageY)/2
1710								}
1711
1712								var dist = Math.sqrt(Math.pow(eo.touches[0].pageX - eo.touches[1].pageX, 2) + Math.pow(eo.touches[0].pageY - eo.touches[1].pageY, 2)) / initD;
1713
1714								var scale = self.scale;
1715								self.scale = normalizeScale(initScale * dist);
1716
1717								self.x = normalizeX(self.x - (pos.x - self.container.el.offset().left - self.x) * (self.scale/scale - 1));
1718								self.y = normalizeY(self.y - (pos.y - self.container.el.offset().top - self.y) * (self.scale/scale - 1));
1719
1720								zoomTo(self.x, self.y, self.scale, 100, 'ease');
1721							}
1722						});
1723
1724						$(document).on('touchend.mapplic', function(e) {
1725							e.preventDefault();
1726							var dragback = null,
1727								eo = e.originalEvent;
1728
1729							if (eo.touches.length == 0) {
1730								clearTimeout(dragback);
1731								$(document).off('touchend.mapplic');
1732								self.map.off('touchmove');
1733								self.map.removeClass('mapplic-dragging');
1734								self.dragging = false;
1735							}
1736							else if (eo.touches.length == 1) {
1737								dragback = setTimeout(function() {
1738									self.map.addClass('mapplic-dragging');
1739									self.dragging = true;
1740
1741									s.stopMomentum();
1742									init1 = {
1743										x: eo.touches[0].pageX - self.x,
1744										y: eo.touches[0].pageY - self.y
1745									};
1746
1747									mouse = {
1748										x: normalizeX(eo.touches[0].pageX - init1.x),
1749										y: normalizeY(eo.touches[0].pageY - init1.y)
1750									};
1751
1752									momentumStep();
1753								}, 60);
1754							}
1755						});
1756					}
1757
1758
1759					// pinch
1760					else if (eo.touches.length == 2) {
1761						self.dragging = true;
1762						self.map.addClass('mapplic-dragging');
1763
1764						s.stopMomentum();
1765
1766						init2 = { x: eo.touches[1].pageX - self.x, y: eo.touches[1].pageY - self.y };
1767						initD = Math.sqrt(Math.pow(init1.x - init2.x, 2) + Math.pow(init1.y - init2.y, 2));
1768						initScale = self.scale;
1769
1770					}
1771				});
1772			}
1773		}
1774
1775		// functions
1776		var processData = function(data) {
1777			self.data = data;
1778			self.g = {};
1779			self.l = {};
1780			var shownLevel = null;
1781
1782			// extend options
1783			self.o = $.extend(self.o, data);
1784			$.each(self.el.data(), function(i, v) { self.o[i] = v; });
1785			self.o.zoommargin = parseFloat(self.o.zoommargin);
1786			self.o.maxscale = parseFloat(self.o.maxscale);
1787
1788			// more text
1789			if (self.o.moretext) self.loc.more = self.o.moretext;
1790
1791			// height of container
1792			if (self.el.data('height')) self.o.height = self.el.data('height');
1793			self.contentWidth = parseFloat(data.mapwidth);
1794			self.contentHeight = parseFloat(data.mapheight);
1795
1796			// limiting to scale 1
1797			self.contentWidth = self.contentWidth * self.o.maxscale;
1798			self.contentHeight = self.contentHeight * self.o.maxscale;
1799
1800			// create container
1801			self.container = new Container().init();
1802
1803			// styles
1804			self.styles = new Styles().init(self.o.styles);
1805
1806			// create minimap
1807			if (self.o.minimap) self.minimap = new Minimap().init();
1808
1809			// create legend
1810			self.legend = new Legend().init();
1811			self.legend.build(data.groups || data.categories);
1812
1813			// directory
1814			self.directory = new Directory().init();
1815
1816			// create sidebar
1817			if (self.o.sidebar) {
1818				self.sidebar = new Sidebar();
1819				self.sidebar.init();
1820			}
1821			else self.container.el.css('width', '100%');
1822
1823			// groups
1824			self.groups = new Groups();
1825			self.groups.addGroups(data.groups || data.categories);
1826
1827			// trigger event
1828			self.el.trigger('mapstart', self);
1829
1830			// iterate through levels
1831			var levelnr = 0,
1832				toload = 0;
1833
1834			if (data.levels) {
1835				$.each(data.levels, function(index, level) {
1836					var source = level.map,
1837						extension = source.substr((source.lastIndexOf('.') + 1)).toLowerCase();
1838
1839					// new map layer
1840					var layer = $('<div></div>').addClass('mapplic-layer').attr('data-floor', level.id).appendTo(self.map);
1841					switch (extension) {
1842
1843						// image formats
1844						case 'jpg': case 'jpeg': case 'png': case 'gif':
1845							var mapimage = $('<div></div>').addClass('mapplic-map-image').appendTo(layer);
1846							$('<img>').attr('src', source).attr('aria-hidden', 'true').addClass('mapplic-map-image').appendTo(mapimage);
1847							if (level.locations) self.addLocations(level.locations, level.id);
1848							break;
1849
1850						// vector format
1851						case 'svg':
1852							toload++;
1853							var mapimage = $('<div></div>').addClass('mapplic-map-image').appendTo(layer);
1854
1855							$('<div></div>').load(source, function() {
1856
1857								// sanitize svg - XSS protection
1858								$('script', this).remove();
1859								mapimage.html($(this).html());
1860
1861								// illustrator duplicate id fix
1862								$(self.o.selector, mapimage).each(function() {
1863									var id = $(this).attr('id');
1864									if (id) $(this).attr('id', id.replace(/_[1-9]+_$/g, ''));
1865								});
1866
1867								// add locations
1868								if (level.locations) self.addLocations(level.locations, level.id);
1869
1870								// click event
1871								$(self.o.selector, mapimage).on('click touchend', function(e) {
1872									var shift = Math.abs(self.firstcoord.x - self.lastcoord.x) + Math.abs(self.firstcoord.y - self.lastcoord.y);
1873									if (!self.dragging || shift < 4) self.showLocation($(this).attr('id'), 600);
1874								});
1875
1876								// autopopulate
1877								if (self.o.autopopulate) {
1878									var ap = [];
1879									$(self.o.selector, mapimage).each(function() {
1880										var id = $(this).attr('id'),
1881											location = self.l[id];
1882
1883										if (!location) {
1884											location = {
1885												id: id,
1886												title: id.charAt(0).toUpperCase() + id.slice(1),
1887												pin: 'hidden'
1888											};
1889											ap.push(location);
1890										}
1891									});
1892									self.addLocations(ap, level.id);
1893								}
1894
1895								// trigger event(s)
1896								self.el.trigger('svgloaded', [mapimage, level.id]);
1897								toload--;
1898								if (toload == 0) mapReady();
1899							});
1900							break;
1901
1902						// others 
1903						default:
1904							alert('File type ' + extension + ' is not supported!');
1905					}
1906
1907					// create new minimap layer
1908					if (self.minimap) self.minimap.addLayer(level);
1909
1910					// shown level
1911					if (!shownLevel || level.show)	shownLevel = level.id;
1912
1913					levelnr++;
1914				});
1915			}
1916
1917			// COMPONENTS
1918			self.tooltips = Array();
1919			if (self.o.lightbox && $.magnificPopup) self.lightbox = new Lightbox().init();
1920			if (self.o.hovertip) self.hovertip = new HoverTooltip().init();
1921			if (self.o.external) self.external = new External().init();
1922			if (self.o.clearbutton) self.clearbutton = new ClearButton().init();
1923			if (self.o.zoombuttons) self.zoombuttons = new ZoomButtons().init();
1924			if (self.o.fullscreen) self.fullscreen = new Fullscreen().init();
1925			if (self.o.developer) self.devtools = new DevTools().init();
1926
1927			self.container.addLevelSwitcher();
1928			self.switchLevel(shownLevel);
1929
1930			if (self.o.portrait === 'true') self.o.portrait = true;
1931
1932			// resize
1933			$(window).resize(function() {
1934				if (self.o.portrait == true || $.isNumeric(self.o.portrait) && self.el.width() < parseFloat(self.o.portrait)) {
1935					self.el.addClass('mapplic-portrait');
1936					if (self.el.hasClass('mapplic-fullscreen')) self.container.el.height($(window).height());
1937					else self.container.el.height(self.container.calcHeight(self.o.height));
1938				}
1939				else {
1940					self.el.removeClass('mapplic-portrait');
1941					self.container.el.height('100%');
1942					self.el.height(self.container.calcHeight(self.o.height));
1943				}
1944
1945				var wr = self.container.el.width() / self.contentWidth,
1946					hr = self.container.el.height() / self.contentHeight;
1947
1948				if (self.o.mapfill) {
1949					if (wr > hr) self.fitscale = wr;
1950					else self.fitscale = hr;
1951				}
1952				else {
1953					if (wr < hr) self.fitscale = wr;
1954					else self.fitscale = hr;
1955				}
1956
1957				if (self.container.oldW != self.container.el.width() || self.container.oldH != self.container.el.height()) {
1958
1959					self.container.oldW = self.container.el.width();
1960					self.container.oldH = self.container.el.height();
1961
1962					self.container.resetZoom();
1963				}
1964			}).resize();
1965
1966			// deeplinking
1967			if (self.o.deeplinking) {
1968				if (history.pushState && (typeof URL == 'function')) self.deeplinking = new Deeplinking();
1969				else self.deeplinking = new DeeplinkingHash();
1970
1971				self.deeplinking.init();
1972			}
1973
1974			// trigger event
1975			if (toload == 0) mapReady();
1976
1977			// controls
1978			if (self.o.zoom) self.container.addControls();
1979			self.firstcoord = self.lastcoord = {};
1980
1981			// link to locations
1982			$(document).on('click', '.mapplic-location', function(e) {
1983				e.preventDefault();
1984				self.showLocation($(this).attr('href').substr(1), 400);
1985				$('html, body').animate({ scrollTop: self.container.el.offset().top }, 400);
1986			});
1987		}
1988
1989		var mapReady = function() {
1990			// separate location array
1991			self.addLocations(self.data.locations);
1992
1993			// apply toggle
1994			self.legend.applyToggles();
1995
1996			// CSV support
1997			if (self.o.csv) { 
1998				if (typeof Papa === 'undefined') {
1999					console.warn('CSV parser missing. Please make sure the library is loaded.');
2000				}
2001				else {
2002					Papa.parse(self.o.csv, {
2003						header: true,
2004						download: true,
2005						encoding: "UTF-8",
2006						skipEmptyLines: true,
2007						complete: function(results, file) {
2008							self.addLocations(results.data);
2009							$('.mapplic-pin', self.map).css({ 'transform': 'scale(' + 1/self.scale + ')' });
2010							if (self.deeplinking) self.deeplinking.check(0);
2011
2012							self.el.trigger('csvready', self);
2013						}
2014					});
2015				}
2016			}
2017
2018			self.container.resetZoom();
2019			if (self.deeplinking) self.deeplinking.check(0);
2020
2021			// landmark mode
2022			if (self.el.data('landmark')) self.o.landmark = self.el.data('landmark');
2023			if (self.o.landmark) {
2024				// Custom settings
2025				self.o.sidebar = false;
2026				self.o.zoombuttons = false;
2027				self.o.deeplinking = false;
2028				self.showLocation(self.o.landmark, 0, true);
2029			}
2030
2031			// trigger event
2032			self.el.trigger('mapready', self);
2033		}
2034
2035		/* PRIVATE METHODS */
2036
2037		// Web Mercator (EPSG:3857) lat/lng projection
2038		var latlngToPos = function(lat, lng) {
2039			var deltaLng = self.data.rightLng - self.data.leftLng,
2040				bottomLatDegree = self.data.bottomLat * Math.PI / 180,
2041				mapWidth = ((self.data.mapwidth / deltaLng) * 360) / (2 * Math.PI),
2042				mapOffsetY = (mapWidth / 2 * Math.log((1 + Math.sin(bottomLatDegree)) / (1 - Math.sin(bottomLatDegree))));
2043
2044			lat = lat * Math.PI / 180;
2045
2046			return {
2047				x: ((lng - self.data.leftLng) * (self.data.mapwidth / deltaLng)) / self.data.mapwidth,
2048				y: (self.data.mapheight - ((mapWidth / 2 * Math.log((1 + Math.sin(lat)) / (1 - Math.sin(lat)))) - mapOffsetY)) / self.data.mapheight
2049			};
2050		}
2051
2052		var estimatedPosition = function(element) {
2053			if (!element || !(element[0] instanceof SVGElement)) return false;
2054
2055			var	bbox = element[0].getBBox();
2056
2057			var	padding = 40,
2058				wr = self.container.el.width() / (bbox.width + padding),
2059				hr = self.container.el.height() / (bbox.height + padding);
2060
2061			return {
2062				x: (bbox.x + bbox.width/2) / self.contentWidth * self.o.maxscale,
2063				y: (bbox.y + bbox.height/2) / self.contentHeight * self.o.maxscale,
2064				scale: Math.min(wr, hr) / self.o.maxscale
2065			}
2066		}
2067
2068		var getIcon = function(name) {
2069			return '<svg class="mapplic-icon mapplic-' + name + '" aria-hidden="true"><use xlink:href="' + self.loc.iconfile + '#' + name + '"></use></svg>';
2070		}
2071
2072		// normalizing x, y and scale
2073		var normalizeX = function(x) {
2074			var minX = (self.container.el.width() - self.contentWidth * self.scale).toFixed(4);
2075			if (minX < 0) {
2076				if (x > self.o.zoommargin) x = self.o.zoommargin;
2077				else if (x < minX - self.o.zoommargin) x = minX - self.o.zoommargin;
2078			}
2079			else x = minX/2;
2080
2081			return x;
2082		}
2083
2084		var normalizeY = function(y) {
2085			var minY = (self.container.el.height() - self.contentHeight * self.scale).toFixed(4);
2086			if (minY < 0) {
2087				if (y > self.o.zoommargin) y = self.o.zoommargin;
2088				else if (y < minY - self.o.zoommargin) y = minY - self.o.zoommargin;
2089			}
2090			else y = minY/2;
2091
2092			return y;
2093		}
2094
2095		var normalizeScale = function(scale) {
2096			if (self.fitscale > 1) return self.fitscale; // no zoom
2097
2098			if (scale <= self.fitscale) scale = self.fitscale;
2099			else if (scale > 1) scale = 1;
2100
2101			// zoom timeout
2102			clearTimeout(self.zoomTimeout);
2103			self.zoomTimeout = setTimeout(function() {
2104				if (self.zoombuttons) self.zoombuttons.update(scale);
2105				if (self.clearbutton) self.clearbutton.update(scale);
2106				if (scale == self.fitscale) {
2107					self.container.coverAll();
2108					if (self.o.closezoomout) self.hideLocation();
2109				}
2110				self.container.revealZoom(scale);
2111			}, 200);
2112
2113			return scale;
2114		}
2115
2116		var moveTimeout = null;
2117
2118		var zoomTo = function(x, y, scale, d) {
2119			d = typeof d !== 'undefined' ? d/1000 : 0;
2120
2121			// move class
2122			self.el.addClass('mapplic-move');
2123			clearTimeout(moveTimeout);
2124			moveTimeout = setTimeout(function() {
2125				self.el.removeClass('mapplic-move');
2126				self.el.trigger('positionchanged', location);
2127			}, 400);
2128
2129			// transforms
2130			self.map.css({
2131				'transition': 'transform ' + d + 's',
2132				'transform': 'translate(' + x.toFixed(3) + 'px ,' + y.toFixed(3) + 'px) scale(' + self.scale.toFixed(3) + ')'
2133			});
2134
2135			if (scale) {
2136				$('.mapplic-pin, .mapplic-tooltip', self.map).css({
2137					'transition': 'transform ' + d + 's',
2138					'transform': 'scale(' + 1/scale + ')'
2139				});
2140			}
2141
2142			if (self.minimap) self.minimap.update(x, y);
2143
2144			// trigger event
2145			self.el.trigger('positionchanged', location);
2146		}
2147
2148		var replaceVars = function(template, location) {
2149			template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
2150				match = match.slice(2, -2);
2151				var sub = match.split('.');
2152				if (sub.length > 1) {
2153					var temp = location;
2154					sub.forEach(function (item) {
2155						if (!temp[item]) {
2156							temp = '{{' + match + '}}';
2157							return;
2158						}
2159						temp = temp[item];
2160					});
2161					return temp;
2162				}
2163				else {
2164					if (!location[match]) return '{{' + match + '}}';
2165					return location[match];
2166				}
2167			});
2168			return template;
2169		}
2170
2171
2172		/* PUBLIC METHODS */
2173		var levelTimeout = null;
2174		self.switchLevel = function(target) {
2175			// no such layer
2176			if (!target) return;
2177
2178			var layer = $('.mapplic-layer[data-floor="' + target + '"]', self.map);
2179
2180			// target layer is already active
2181			if (layer.hasClass('mapplic-visible')) return;
2182
2183			// show target layer
2184			layer.removeClass('mapplic-hidden');
2185
2186			setTimeout(function() {
2187				var old = $('.mapplic-layer.mapplic-visible', self.map).removeClass('mapplic-visible');
2188				layer.addClass('mapplic-visible');
2189				
2190				clearTimeout(levelTimeout);
2191				levelTimeout = setTimeout(function() { 
2192					$('.mapplic-layer:not([data-floor="' + target + '"])', self.map).addClass('mapplic-hidden');
2193				}, 300);
2194
2195				// slide animation
2196				if (self.o.animations) {
2197					var found = false;
2198					$('.mapplic-layer', self.map).each(function() {
2199						if ($(this).data('floor') == target) {
2200							$(this).removeClass('mapplic-layer-up').removeClass('mapplic-layer-down');
2201							found = true;
2202						}
2203						else if (found) $(this).addClass('mapplic-layer-up');
2204						else $(this).addClass('mapplic-layer-down');
2205					});
2206				}
2207			}, 1);
2208
2209			// show target minimap layer
2210			if (self.minimap) self.minimap.show(target);
2211
2212			self.level = target;
2213
2214			// trigger event
2215			self.el.trigger('levelswitched', target);
2216		}
2217
2218		self.mouseWheel = function(e, delta) {
2219			e.preventDefault();
2220			self.container.stopMomentum();
2221
2222			var scale = self.scale;
2223
2224			self.scale = normalizeScale(scale + scale * delta / 5);
2225
2226			self.x = normalizeX(self.x - (e.pageX - self.container.el.offset().left - self.x) * (self.scale/scale - 1));
2227			self.y = normalizeY(self.y - (e.pageY - self.container.el.offset().top - self.y) * (self.scale/scale - 1));
2228
2229			zoomTo(self.x, self.y, self.scale, 200, 'ease');
2230		}
2231
2232		self.addTooltip = function(location, check) {
2233			var tooltip = new Tooltip().init(location, check);
2234			self.tooltips.push(tooltip);
2235
2236			return tooltip.wrap[0];
2237		}
2238
2239		self.closeTooltips = function() {
2240			self.tooltips.forEach(function(t, i) {
2241				t.hide();
2242				self.tooltips.splice(i, 1);
2243			});
2244		}
2245
2246		self.moveTo = function(x, y, s, duration, ry) {
2247			duration = typeof duration !== 'undefined' ? duration : 400;
2248			ry = typeof ry !== 'undefined' ? ry : 0.5;
2249			s = typeof s !== 'undefined' ? s : self.scale/self.fitscale;
2250
2251			self.container.stopMomentum();
2252
2253			self.scale = normalizeScale(s);
2254			self.x = normalizeX(self.container.el.width() * 0.5 - self.scale * self.contentWidth * x);
2255			self.y = normalizeY(self.container.el.height() * ry - self.scale * self.contentHeight * y);
2256
2257			zoomTo(self.x, self.y, self.scale, duration);
2258		}
2259
2260		self.bboxZoom = function(element) {
2261			var pos = estimatedPosition(element);
2262			if (!pos) return false;
2263
2264			self.moveTo(pos.x, pos.y, pos.scale, 600);
2265			return true;
2266		}
2267
2268		// adding locations
2269		self.addLocations = function(locations, levelid) {
2270			$.each(locations, function(index, location) {
2271				if (location.id) self.addLocation(location, levelid);
2272			});
2273		}
2274
2275		self.addLocation = function(location, levelid) {
2276			// jump if location ID exists
2277			if (self.l[location.id]) return true;
2278
2279			// building the location object
2280			self.l[location.id] = location;
2281
2282			// groups
2283			var groups = location.category;
2284			if (groups) {
2285				if (typeof groups == 'string') groups = groups.toString().split(',');
2286				groups.forEach(function(group) { if (self.g[group]) self.g[group].nr++; });
2287			}
2288
2289			// cascaded fill
2290			var fill = (location && location.fill) ||
2291				(location.category && self.g[location.category] && self.g[location.category].color) ||
2292				(location.category && location.category[0] && self.g[location.category[0]] && self.g[location.category[0]].color) ||
2293				self.o.fillcolor || false;
2294
2295			// cascade style to attribute styleD!
2296			location.styled = (location && location.style) ||
2297				(location.category && self.g[location.category] && self.g[location.category].style) ||
2298				(location.category && location.category[0] && self.g[location.category[0]] && self.g[location.category[0]].style) ||
2299				self.o.defaultstyle || false;
2300			
2301			if (fill) location.color = fill;
2302			else if (location.styled) location.color = location.styled.base;
2303
2304			// interactive element
2305			var elem = $('[id^=MLOC] >
2305 *[id="' + location.id + '"], [id^=landmark] > *[id="' + location.id + '"]', self.map);
2306			if (elem.length > 0) {
2307				location.el = elem;
2308				location.el.addClass('mapplic-clickable');
2309
2310				if (location.styled) {
2311					location.el.addClass(location.styled);
2312				}
2313				else if (fill) {
2314					location.el.css('fill', fill);
2315					$('> *', location.el).css('fill', fill);
2316				}
2317			}
2318
2319			// first level if not set
2320			if (!location.level) {
2321				if (levelid) location.level = levelid;
2322				else if (location.el && location.el.closest('.mapplic-layer').data('floor')) location.level = location.el.closest('.mapplic-layer').data('floor');
2323				else location.level = self.data.levels[0].id;
2324			}
2325
2326			// description vars
2327			if (location.description) location.description = replaceVars(location.description, location);
2328
2329			// geolocation
2330			if (location.lat && location.lng) {
2331				var pos = latlngToPos(location.lat, location.lng);
2332				location.x = pos.x;
2333				location.y = pos.y;
2334			}
2335
2336			// estimated position
2337			if ((!location.x || !location.y) && elem) {
2338				var pos = estimatedPosition(location.el);
2339				location.x = pos.x;
2340				location.y = pos.y;
2341			}
2342
2343			// marker
2344			if (!location.pin) location.pin = self.o.marker;
2345			location.marker = self.addMarker(location);
2346
2347			// reveal mode
2348			if (location.action == 'reveal') $('.mapplic-pin[data-location^=' + location.id + ']', self.map).css('visibility', 'hidden');
2349
2350			if (self.sidebar) self.sidebar.countCategory();
2351		}
2352
2353		self.addMarker = function(location) {
2354			// hidden marker
2355			if (location.pin.indexOf('hidden') != -1) return false;
2356
2357			var parent = $('.mapplic-layer[data-floor=' + location.level + '] .mapplic-map-image', self.el);
2358			var marker = $('<a></a>').addClass('mapplic-pin').addClass(location.pin.replace('hidden', '')).attr('aria-label', location.title + ' marker').css({'top': (location.y * 100) + '%', 'left': (location.x * 100) + '%'}).appendTo(parent);
2359			marker.on('click touchend', function(e) {
2360				if (e.cancelable) e.preventDefault();
2361
2362				var shift = Math.abs(self.firstcoord.x - self.lastcoord.x) + Math.abs(self.firstcoord.y - self.lastcoord.y);
2363				if (!self.dragging || shift < 4) self.showLocation(location.id, 600);
2364			});
2365
2366			if (location.label) {
2367				if (location.label.match(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g)) marker.css('background-image', 'url(' + location.label + ')');
2368				else $('<span><span>' + location.label + '</span></span>').appendTo(marker);
2369			}
2370			if (location.reveal) marker.attr('data-reveal', location.reveal).css('visibility', 'hidden');
2371			if (location.category) {
2372				location.category = location.category.toString();
2373				marker.attr('data-category', location.category);
2374			}
2375			marker.attr('data-location', location.id);
2376
2377			if (self.o.zoom && self.o.mousewheel) marker.on('mousewheel DOMMouseScroll', self.mouseWheel);
2378			if (location.styled) marker.addClass(location.styled);
2379			if (location.color && location.pin.indexOf('pin-text') > -1) marker.css('color', location.color);
2380			else if (location.color) marker.css({'background-color': location.color, 'border-color': location.color });
2381			
2382			location.el = marker;
2383			return marker;
2384		}
2385
2386		// removing locations
2387		self.removeLocations = function() {
2388			$.each(self.l, function(i, location) {
2389				self.removeLocation(location.id);
2390			});
2391		}
2392
2393		self.removeLocation = function(location) {
2394			if (location.id) location = location.id;
2395			delete self.l[location];
2396			$('.mapplic-pin[data-location="' + location + '"]', self.map).remove();
2397			$('.mapplic-tooltip[data-location="' + location + '"]', self.el).remove();
2398			$('.mapplic-list-location[data-location="' + location + '"]', self.el).remove();
2399			$('svg #' + location, self.map).removeClass();
2400		}
2401
2402		self.getLocationData = function(id) {
2403			return self.l[id];
2404		}
2405
2406		self.showLocation = function(id, duration, check) {
2407			var location = self.location = self.l[id];
2408			if (!location) return false;
2409
2410			// trigger event
2411			self.el.trigger('locationopen', location);
2412
2413			var action = (location.action && location.action != 'default') ? location.action : self.o.action;
2414			if (action == 'disabled') return false;
2415
2416			var content = null;
2417			self.closeTooltips();
2418
2419			switch (action) {
2420				case 'open-link':
2421					window.location.href = location.link;
2422					return false;
2423				case 'open-link-new-tab':
2424					window.open(location.link);
2425					self.location = null;
2426					return false;
2427				case 'select':
2428					if (location.el) {
2429						if (location.el.hasClass('mapplic-active')) {
2430							location.el.removeClass('mapplic-active');
2431							if (location.list) location.list.removeClass('mapplic-active');
2432						}
2433						else {
2434							location.el.addClass('mapplic-active');
2435							if (location.list) location.list.addClass('mapplic-active');
2436						}
2437					}
2438					return false;
2439				case 'none':
2440					self.hideLocation();
2441					self.switchLevel(location.level);
2442					if (!self.bboxZoom(location.el)) self.zoomLocation(location);
2443					break;
2444				case 'reveal':
2445					self.hideLocation();
2446					self.switchLevel(location.level);
2447					self.container.revealChild(location);
2448					if (self.o.zoom) self.bboxZoom(location.el); 
2449					break;
2450				case 'external':
2451					self.hideLocation();
2452					self.switchLevel(location.level);
2453					if (!self.bboxZoom(location.el)) self.zoomLocation(location);
2454					if (self.external) self.external.show(location);
2455					break;
2456				case 'lightbox':
2457					self.hideLocation();
2458					self.switchLevel(location.level);
2459					content = self.lightbox.show(location);
2460					break;
2461				case 'image':
2462					self.hideLocation();
2463					self.switchLevel(location.level);
2464					self.lightbox.showImage(location);
2465					break;
2466				case 'route':
2467					check = true;
2468					break;
2469				default:
2470					self.hideLocation();
2471					self.switchLevel(location.level);
2472					setTimeout(function() { content = self.addTooltip(location, check); }, 2);
2473			}
2474
2475			self.location = self.l[id];
2476
2477			// active state
2478			$('.mapplic-active', self.scope).removeClass('mapplic-active');
2479			if (location.el) location.el.addClass('mapplic-active');
2480			if (location.list) location.list.addClass('mapplic-active');
2481
2482			// deeplinking
2483			if ((self.deeplinking) && (!check)) self.deeplinking.update(id);
2484
2485			// trigger event
2486			self.el.trigger('locationopened', [location, content]);
2487		}
2488
2489		self.zoomLocation = function(loc) {
2490			var zoom = loc.zoom ? parseFloat(loc.zoom)/self.o.maxscale : 1;
2491			if (self.o.zoom) self.moveTo(loc.x, loc.y, zoom, 600);
2492		}
2493
2494		self.hideLocation = function() {
2495			$('.mapplic-active', self.scope).removeClass('mapplic-active');
2496			if (self.deeplinking && self.deeplinking.resolved) self.deeplinking.clear();
2497			if (self.external) self.external.hide();
2498			self.closeTooltips();
2499			self.location = null;
2500
2501			// trigger event
2502			self.el.trigger('locationclosed');
2503		}
2504
2505		self.updateLocation = function(id) {
2506			// remove + add
2507			var location = self.l[id];
2508
2509			if ((location.id == id) && (location.el.is('a')))  {
2510				// Geolocation
2511				if (location.lat && location.lng) {
2512					var pos = latlngToPos(location.lat, location.lng);
2513					location.x = pos.x;
2514					location.y = pos.y;
2515				}
2516				
2517				var top = location.y * 100,
2518					left = location.x * 100;
2519				location.el.css({'top': top + '%', 'left': left + '%'});
2520			}
2521		}
2522
2523	};
2524
2525	// jQuery plugin
2526	$.fn.mapplic = function(options) {
2527
2528		return this.each(function() {
2529			var element = $(this);
2530
2531			// plugin already initiated on element
2532			if (element.data('mapplic')) return;
2533
2534			var instance = (new Mapplic(element)).init(options);
2535
2536			// store plugin object in element's data
2537			element.data('mapplic', instance);
2538		});
2539	};
2540
2541})(jQuery);
2542
2543// call plugin on map instances
2544jQuery(document).ready(function($) {
2545	$('[id^=mapplic-id]').mapplic();
2546
2547	// dynamic element
2548	var observer = new MutationObserver(function(mutations) {
2549		mutations.forEach(function(mutation) {
2550			var newNodes = mutation.addedNodes;
2551			if (newNodes !== null) {
2552				$(newNodes).each(function() {
2553					var node = $(this);
2554					if (node.is('[id^=mapplic-id]')) node.mapplic();
2555				});
2556			}
2557		});
2558	});
2559
2560	observer.observe(document, {attributes: false, childList: true, characterData: false, subtree: true});
2561});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.