PageSourceSearch

https://ymcacampcullen.org/wp-content/plugins/mapplic/core/mapplic.js?ver=7.0.1

js ymcacampcullen.org collected 2026-10-06 07:00:59 UTC 80,705 bytes, 2,524 lines download raw bytes

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

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.