403Webshell
Server IP : 89.248.107.232  /  Your IP : 216.73.217.70
Web Server : Apache
System : Linux host2.kasilh.com 5.14.0-687.36.1.el9_8.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Aug 7 05:40:49 EDT 2026 x86_64
User : seg ( 10005)
PHP Version : 7.4.33
Disable Function : opcache_get_status
MySQL : OFF  |  cURL : ON  |  WGET : OFF  |  Perl : OFF  |  Python : OFF  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /var/www/vhosts/seg-sa.es/grupo-seg.com/wp-content/plugins/mapplic-geo/js/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/vhosts/seg-sa.es/grupo-seg.com/wp-content/plugins/mapplic-geo/js/mgeo.js
/*
 * Mapplic GEO by @sekler
 * Version 1.0.3
 * https://www.mapplic.com/geo
 */

(function() {
	"use strict";

	function MapplicGeo(element) {
		let self = this;
		
		this.o = {
			extent: [-18878672.443631265, -9326678.110352932, 21196344.241947223, 19298333.807917416], // world
			center: [23.41, 46.82],
			minZoom: 1,
			maxZoom: 18,

			rotation: false,
			rotate: 0,

			data: 'data.json',
			csv: false,

			sidebar: false,
			search: true,
			searchfields: ['title', 'about'],
			sortbytitle: false,
			padding: [0, 0, 0, 0],
			portrait: 800,
			rightSidebar: false,
			portraitSidebarAbove: false,
			thumbnail: true,
			minHeight: 0,
			maxHeight: Infinity,

			improvedPerformance: false,
			hidpi: true,
			constrainResolution: false,
			renderBuffer: 1000,

			attribution: true,
			attributionCollapsible: false,
			deeplinking: true,

			mouseWheel: true,
			fullscreen: false,
			mouseWheelShift: false,
			zoomButtons: false,
			resetButton: false,
			levelselector: false,

			maptilerkey: false,
			mapboxtoken: false,
			bingmapskey: false,

			showFullExtent: true,
			globalSample: false,
			blurOnZoomOut: true,
			resetZoomOnBlur: false,
			moreText: false,

			hoverTooltip: true,
			hoverTooltipFollow: false,

			customcss: null
		}

		this.localize = {
			search: 'Search',
			more: 'More',
			pdir: 'mgeo/'
		}

		this.wp = false;
		this.data = null;

		this.init = function(options) {
			// overwrite options
			Object.assign(this.o, options);

			self.el = element;

			if (typeof mgeo_localize !== 'undefined') {
				Object.assign(this.localize, mgeo_localize);
				this.wp = true;
			}

			self.preloader = document.createElement('div');
			self.preloader.classList.add('mgeo-preloader');
			self.el.appendChild(self.preloader);

			if (self.el.dataset.json) self.o.data = self.el.dataset.json;

			if (self.el.dataset.jsonstr) {
				this.processData(JSON.parse(self.el.dataset.jsonstr));
				self.el.removeAttribute('data-jsonstr');
			}
			else self.getJSON(self.o.data).then(this.processData);
		}

		this.processData = function(json) {
			self.data = json;

			if (json.settings) delete json.settings.data;
			Object.assign(self.o, json.settings);

			if (self.o.moreText) self.localize.more = self.o.moreText;
			if (self.o.improvedPerformance) {
				self.o.hidpi = false;
				self.o.renderBuffer = 200;
			}

			// modules
			self.dir = new Directory().init();
			self.styles = new Styles().init();
			self.container = new Container().init();
			self.actions = new Actions().init();
			if (self.o.sidebar) self.sidebar = new Sidebar().init();
			if (self.o.deeplinking) self.deeplinking = new Deeplinking().init();

			self.styles.addStyles(json.styles);
			self.dir.registerGroups(json.groups);
			self.container.addLayers(json.layers);
			self.dir.resolveLocations(json.locations);
			self.container.addMarkers(json.locations);
			self.container.resize();

			if (self.o.sidebar) {
				if (self.o.search) self.sidebar.addGroups(json.groups);
				self.sidebar.addLocations(json.locations);
			}

			self.getCSV(self.o.csv).then(function(csv) {
				self.dir.resolveLocations(csv.data, true);
				self.container.addMarkers(csv.data);
				if (self.o.sidebar) self.sidebar.addLocations(csv.data);
				self.ready();
			}).catch(function(error) {
				self.ready();
			});
		}

		this.getJSON = function(source) {
			return new Promise(function(resolve, reject) {
				if (typeof source === 'object') resolve(source);
				else if (typeof source === 'string') {
					var headers = new Headers();
					headers.append('pragma', 'no-cache');
					headers.append('cache-control', 'no-cache');

					fetch(source, { headers: headers }).then(response => response.json()).then(data => {
						resolve(data);
					}).catch(function(error) {
						resolve({});
					});
				}
				else resolve('invaild json');
			});
		}

		this.getCSV = function(source) {
			return new Promise(function(resolve, reject) {
				if (typeof Papa === 'undefined') reject('CSV parser missing');
				Papa.parse(source, {
					header: true,
					download: true,
					encoding: 'UTF-8',
					skipEmptyLines: true,
					transformHeader: function(h) { return h.trim().toLowerCase(); },
					complete: function(results) { resolve(results); },
					error: function(err) { reject(err); }
				});
			});
		}

		this.ready = function() {
			setTimeout(function() { self.deeplinking?.check(); }, 400);

			self.el.dispatchEvent(new CustomEvent('mapready', { detail: self }));

			self.preloader.style.opacity = 0;
			setTimeout(function() {
				self.el.removeChild(self.preloader);
			}, 200);
		}

		// sidebar module
		function Sidebar() {
			const s = this;

			this.el = null;
			this.header = null;
			this.list = null;
			this.toggle = null;
			this.tags = null;
			this.filters = null;
			this.popup = null;

			this.init = function() {
				this.el = document.createElement('div');
				this.el.classList.add('mgeo-sidebar');

				if (self.o.rightSidebar) this.el.classList.add('mgeo-sidebar-right');
				
				self.container.addSidebarControl(this.el);

				let el = document.createElement('div');
				el.classList.add('mgeo-sidebar-dir');
				this.el.appendChild(el);

				// search markup
				if (self.o.search) {
					this.header = document.createElement('div');
					this.header.classList.add('mgeo-sidebar-header');
					el.appendChild(this.header);

					const magnifier = self.styles.getIcon('magnifier');
					this.header.appendChild(magnifier);

					const clear = document.createElement('button');
					clear.classList.add('mgeo-filter-clear');
					clear.appendChild(self.styles.getIcon('cross'));
					this.header.appendChild(clear);

					const wrap = document.createElement('div');
					this.header.appendChild(wrap);

					const input = document.createElement('input');
					input.classList.add('mgeo-search-input');
					input.setAttribute('type', 'text');
					input.setAttribute('spellcheck', 'false');
					input.setAttribute('placeholder', self.localize.search);
					input.addEventListener('keyup', function(e) {
						let change = true;

						if (input.value) {
							if (self.dir.filters['keyword'] != input.value) self.dir.filters['keyword'] = input.value.replace(/\s+/g, ' ').trim(); // remove redundant spaces
							else change = false;
						}
						else delete self.dir.filters['keyword'];

						if (change) {
							s.el.classList.toggle('mgeo-sidebar-header-opened', input.value.length < 1);

							s.search();
							s.refreshCounts();
						}
					});
					input.addEventListener('focus', function() { if (input.value.length < 1) s.el.classList.add('mgeo-sidebar-header-opened'); });
					document.addEventListener('click', function(e) { if (s.header != e.target && !s.header.contains(e.target)) s.el.classList.remove('mgeo-sidebar-header-opened'); });
					wrap.appendChild(input);

					clear.addEventListener('click', function(e) {
						e.preventDefault();
						input.value = '';
						s.tags.innerHTML = '';
						self.dir.filters = {};
						s.el.classList.remove('mgeo-sidebar-tagsrow');
						s.search();
						s.refreshCounts();
					});

					this.toggle = document.createElement('button');
					this.toggle.classList.add('mgeo-search-toggle');
					this.toggle.appendChild(self.styles.getIcon('filter'));
					this.toggle.addEventListener('click', function(e) {
						e.preventDefault();
						s.el.classList.toggle('mgeo-sidebar-header-opened');
					});
					this.header.appendChild(this.toggle);

					this.tags = document.createElement('div');
					this.tags.classList.add('mgeo-sidebar-tags');
					this.header.appendChild(this.tags);

					this.filters = document.createElement('div');
					this.filters.classList.add('mgeo-filters');
					this.header.appendChild(this.filters);

					const dim = document.createElement('div');
					dim.classList.add('mgeo-sidebar-dim');
					el.appendChild(dim);
				}
				else this.el.classList.add('mgeo-sidebar-nosearch');

				// list markup
				this.list = document.createElement('ul');
				this.list.classList.add('mgeo-sidebar-list');
				el.appendChild(this.list);

				return this;
			}

			this.search = function() {
				self.dir.search(this.list);

				if (Object.keys(self.dir.filters).length === 0) self.el.classList.remove('mgeo-filter-active');
				else self.el.classList.add('mgeo-filter-active');
			}

			this.refreshCounts = function() {
				if (!this.filters) return false;

				const filters = this.filters.querySelectorAll('.mgeo-filter');

				for (const filter of filters) {
					let count = 0;
					const items = s.list.querySelectorAll('.mgeo-dir-item:not(.mgeo-hidden)');
					for (const item of items) {
						const location = self.dir.l[item.dataset.location];
						if (location.res.group == filter.dataset.filter) count++;
					}
					
					filter.querySelector('.mgeo-count').innerHTML = '(' + count + ')';
				}
			}

			this.removeLocation = function(id) {
				this.list.querySelector('.mgeo-dir-item[data-location="' + id + '"]').remove();
			}

			this.addLocation = function(location) {
				if (String(location.res.hide) != 'true' && !location.res.disabled) {
					let item = self.dir.getItem(location);
					if (item) this.list.appendChild(item);

					if (self.o.sortbytitle) self.dir.sortBy(this.list, 'title');
					this.refreshCounts();
				}
			}

			this.addLocations = function(locations) {
				for (let id in locations) {
					let location = locations[id];

					if (String(location.res.hide) != 'true' && !location.res.disabled && location.id != location.sample) {
						let item = self.dir.getItem(location);
						if (item) this.list.appendChild(item);
					}
				}

				if (self.o.sortbytitle) self.dir.sortBy(this.list, 'title');
				this.refreshCounts();
			}

			this.addTag = function(group) {
				const tag = document.createElement('button');
				let groups = [];

				if (self.dir.filters['group']) groups = self.dir.filters['group'].split(',');

				if (!groups.includes(group.id)) {
					groups.push(group.id);
					self.dir.filters['group'] = groups.toString();

					tag.classList.add('mgeo-tag');
					tag.dataset.filter = group.id;
					tag.innerHTML = group.title;
					if (group.color) tag.style.backgroundColor = group.color;
					this.tags.appendChild(tag);

					tag.appendChild(self.styles.getIcon('cross'));

					tag.addEventListener('click', function(e) {
						e.preventDefault();
						s.removeTag(group.id);
						s.el.classList.remove('mgeo-sidebar-header-opened');
					});

					s.search();

					this.el.classList.remove('mgeo-sidebar-header-opened');
					this.el.classList.add('mgeo-sidebar-tagsrow');
				}
			}

			this.removeTag = function(id) {
				let groups = [];

				if (self.dir.filters['group']) {
					groups = self.dir.filters['group'].split(',');
					let i = groups.indexOf(id);
					if (i > -1) {
						groups.splice(i, 1)
						self.dir.filters['group'] = groups.toString();
					}
					if (groups.length < 1) delete self.dir.filters['group'];
				}

				let tag = this.tags.querySelector('button[data-filter="' + id + '"]');
				this.tags.removeChild(tag);

				s.search();

				if (this.tags.querySelectorAll('.mgeo-tag').length < 1) this.el.classList.remove('mgeo-sidebar-tagsrow');
			}

			this.addGroups = function(groups) {
				if (!groups) return false;

				if (groups.length > 0) this.toggle.style.display = 'block';

				const ul = document.createElement('ul');
				let added = false;

				groups.forEach(function(group) {
					if (group.hide == 'true') return;

					const item = document.createElement('li');
					item.classList.add('mgeo-group', 'mgeo-filter');
					item.dataset.filter = group.id;

					const link = document.createElement('a');
					link.addEventListener('click', function() { s.addTag(group); });
					item.appendChild(link);

					const thumb = self.dir.getThumbnail(group.title, group.thumbnail);
					link.appendChild(thumb);
					if (group.color && thumb) thumb.style.backgroundColor = thumb.style.borderColor = group.color;

					const title = document.createElement('h4');
					title.classList.add('mgeo-group-title');
					title.innerHTML = group.title;
					link.appendChild(title);

					const count = document.createElement('span');
					count.classList.add('mgeo-count');
					count.innerHTML = '(0)';
					link.appendChild(count);

					if (group.about) {
						const about = document.createElement('span');
						about.classList.add('mgeo-dir-about');
						about.innerHTML = group.about;
						link.appendChild(about);
					}
					else title.classList.add('mgeo-title-margin');

					ul.appendChild(item);
					added = true;
				});

				if (added) this.el.classList.add('mgeo-sidebar-filterable');
				if (this.filters) this.filters.appendChild(ul);
			}

			this.addPopup = function(location, small = false) {
				if (!location) return false;

				if (this.popup) this.hidePopup();

				this.popup = document.createElement('div');
				this.popup.classList.add('mgeo-popup');
				this.popup.dataset.location = location.id;

				let container = document.createElement('div');
				container.classList.add('mgeo-popup-container');

				let close = document.createElement('button');
				close.classList.add('mgeo-close');
				close.appendChild(self.styles.getIcon('cross'));
				close.addEventListener('click', function(e) {
					e.preventDefault();
					s.hidePopup();
					return false;
				});
				container.appendChild(close);

				if (location.res.image) {
					let ic = document.createElement('div');
					ic.classList.add('mgeo-popup-image');
					
					let image = document.createElement('img');
					image.src = location.res.image;
					ic.appendChild(image);
					this.popup.appendChild(ic);
				}

				if (location.res.title) {
					let title = document.createElement('h1');
					title.classList.add('mgeo-title');
					title.innerHTML = location.res.title;
					container.appendChild(title);
				}

				if (location.res.about) {
					let about = document.createElement('h4');
					about.classList.add('mgeo-about');
					about.innerHTML = location.res.about;
					container.appendChild(about);
				}

				if (location.res.desc) {
					let content = document.createElement('div');
					content.classList.add('mgeo-popup-content');
					content.innerHTML = location.res.desc;
					container.appendChild(content);
				}

				let buttons = document.createElement('div');
				buttons.classList.add('mgeo-buttons');
				container.appendChild(buttons);
				
				if (location.res.link) {
					let more = document.createElement('a');
					more.classList.add('mgeo-more');
					more.href = location.res.link;
					more.innerHTML = self.localize.more;
					if (location.res.fill) more.style.backgroundColor = location.res.fill;
					buttons.appendChild(more);
				}

				this.popup.appendChild(container);

				this.el.prepend(this.popup);
				this.el.classList.add('mgeo-sidebar-popup');

				self.el.dispatchEvent(new CustomEvent('popuprender', { detail: s.popup }));
			}

			this.hidePopup = function() {
				if (!this.popup) return false;
				
				this.el.removeChild(this.popup);
				this.popup = null;
				this.el.classList.remove('mgeo-sidebar-popup');
				self.actions.blur();
			}
		}

		// styles module
		function Styles() {
			const s = this;

			this.s = [];

			this.icons = {
				'cross': {
					path: 'M8,0.809L7.191,0L4,3.191L0.809,0L0,0.809L3.191,4L0,7.191L0.809,8L4,4.809L7.191,8L8,7.191L4.809,4L8,0.809z',
					viewbox: '0 0 8 8'
				},
				'magnifier': {
					path: 'M15.967,14.803l-4.253-4.285c0.848-1.103,1.364-2.477,1.379-3.971C13.093,2.937,10.156,0,6.546,0S0,2.937,0,6.546c0,3.609,2.937,6.546,6.546,6.546c1.477,0,2.84-0.507,3.94-1.349L14.772,16L15.967,14.803z M1.64,6.579c0-2.706,2.201-4.907,4.907-4.907c2.675,0,4.876,2.203,4.906,4.907c0,2.706-2.2,4.905-4.906,4.905S1.64,9.284,1.64,6.579z',
					viewbox: '0 0 16 16'
				},
				'filter': {
					path: 'M1,11h7v2H1V11z M8,3v2h7V3H8z M4,7h2V1H4v2H1v2h3V7z M12,9h-2v6h2v-2h3v-2h-3V9z',
					viewbox: '0 0 16 16'
				},
				'reset': {
					path: 'M11,3H5l3-3L11,3z M5,13l6,0.01L8,16L5,13z M3,11L0,8l3-3V11z M13,5l3,3l-3,3V5z M8,5C6.34,5,5,6.34,5,8s1.34,3,3,3s3-1.34,3-3S9.66,5,8,5z M8,10c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S9.1,10,8,10z',
					viewbox: '0 0 16 16'
				},
				'compass': {
					path: 'M8,4 5,11 8,9 11,11z',
					viewbox: '0 0 16 16'
				}
			}

			this.init = function() {
				if (self.o.customcss) {
					const style = document.createElement('style');
					style.innerHTML = self.o.customcss;
					document.body.appendChild(style);
				}
				return this;
			}

			this.addStyles = function(styles) {
				if (!styles) return false;

				for (const style of styles) this.s[style.class] = style;
			}

			this.getIcon = function(name) {
				const icon = this.icons[name];
				if (!icon) return null;

				const ns = 'http://www.w3.org/2000/svg';
				const svg = document.createElementNS(ns, 'svg');
				svg.classList.add('mgeo-icon', 'mgeo-icon-' + name);
				svg.setAttribute('viewBox', icon.viewbox);

				const path = document.createElementNS(ns, 'path');
				path.setAttribute('d', icon.path);
				svg.appendChild(path);

				return svg;
			}
		}

		// deeplinking module
		function Deeplinking() {
			const s = this;
			let params;

			this.init = function() {
				params = new URLSearchParams(window.location.search);
				window.onpopstate = function() { s.check(); }
				return this;
			}

			this.set = function(key, value) {
				params.set(key, value);
				this.update();
			}

			this.delete = function(key) {
				params.delete(key);
				this.update();
			}

			this.check = function() {
				params = new URLSearchParams(window.location.search);
				const location = self.dir.l[params.get('location')];

				if (location) self.actions.focus(location, true);
				else self.actions.blur(true);
			}

			this.update = function() {
				window.history.pushState({}, '', `${window.location.pathname}?${params}`);
			}
		}

		// actions module
		function Actions() {
			this.focused = null;
			this.a = {
				'': '(Default)',
				'none': 'None',
				'zoom': 'Zoom',
				'tooltip': 'Tooltip',
				'sidebar': 'Sidebar popup',
				'open-link': 'Open link',
				'open-link-new-tab': 'Open link in new tab',
				'isolate': 'Isolate layer'
			}

			this.init = function() { return this; }

			this.focus = function(location, deep = false) {
				if (!location) return false;

				this.blur();

				this.focused = location;
				self.container.focusLocation(location);

				switch (location.res.action) {
					case 'none':
						return false;
						break;
					case 'zoom':
						self.container.zoomTo(location);
						return false;
						break;
					case 'isolate':
						self.container.zoomTo(location); // isolation fires when needed
						return false;
						break;
					case 'open-link':
						window.location.href = location.link;
						return false;
						break;
					case 'open-link-new-tab':
						window.open(location.link, '_blank');
						return false;
						break;
					case 'sidebar':
						if (self.sidebar) {
							self.container.addTooltip(location, true);
							self.sidebar.addPopup(location);
							self.container.zoomTo(location);
							break;
						}
					default: 
						const tooltip = self.container.addTooltip(location);
						self.container.zoomTo(location, tooltip.offsetHeight);
				}

				if (self.deeplinking && !deep) self.deeplinking.set('location', location.id);

				self.el.dispatchEvent(new CustomEvent('locationfocus', { detail: { location: location } }));
			}

			this.blur = function(deep = false) {
				if (!this.focused) return false;

				self.container.blurLocation(this.focused);
				this.focused = null;

				self.container.hideTooltip();
				self.sidebar?.hidePopup();
				if (self.deeplinking && !deep) self.deeplinking.delete('location');
				if (self.o.resetZoomOnBlur) self.container.resetZoom();

				self.el.dispatchEvent(new CustomEvent('locationblur'));
			}
		}

		// directory module
		function Directory() {
			const s = this;

			this.filters = {};
			this.l = {}; // locations
			this.g = {}; // groups
			this.def = {};

			this.init = function() {
				return this;
			}

			this.replaceVars = function(template, location) {
				if (!location.id || !template) return false;
				template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
					match = match.slice(2, -2).toLowerCase();
					var sub = match.split('.');
					if (sub.length > 1) {
						var temp = location;
						sub.forEach(function(item) {
							if (!temp[item]) {
								temp = '{{' + match + '}}';
								return;
							}
							temp = temp[item];
						});
						return temp;
					}
					else {
						if (!location[match]) return '{{' + match + '}}';
						return location[match];
					}
				});
				return template;
			}

			this.search = function(list = null) {
				for (const id in s.l) { // loop locations
					let location = s.l[id],
						matched = true;

					for (const attr in s.filters) { // loop filters
						let filter = s.filters[attr];
						let current = false;

						if (attr == 'keyword') { // search
							self.o.searchfields.forEach(function(field) { // loop fields
								if (location.res[field] && !current) current = !(s.normalizeString(location.res[field]).indexOf(s.normalizeString(filter)) == -1);
							});
						}
						else if (location.res.group && attr == 'group') { // groups
							let groups = location.res.group;
							if (typeof groups == 'string') groups = groups.split(',');

							groups.forEach(function(group) {
								if (self.dir.filters['group'].split(',').includes(group)) current = true;
							});
						}

						matched = matched && current;
					}

					// show/hide list
					let item = list.querySelector('.mgeo-dir-item[data-location="' + location.id + '"]');
					if (item) {
						if (matched) item.classList.remove('mgeo-hidden');
						else item.classList.add('mgeo-hidden');
					}

					// highlight feature
					if (Object.keys(s.filters).length === 0) self.container.blurLocation(location);
					else if (matched) self.container.highlightLocation(location);
					else self.container.disableLocation(location);
				}
			}

			this.getDirectory = function(attribute, pattern, sortby = false) {
				const regex = new RegExp(pattern, 'i');
				const dir = document.createElement('ul');

				for (var id in s.l) {
					let location = s.l[id];

					if (attribute && pattern && !regex.test(location[attribute])) return true; // skip if no match
					if (String(location.hide) != 'true') dir.appendChild(s.getItem(location));
				}

				if (sortby) this.sortBy(dir, sortby);

				return dir;
			}

			this.getThumbnail = function(name, field) {
				let elem = null;

				if (field) {
					if (field.match(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g)) {
						elem = document.createElement('img');
						elem.classList.add('mgeo-thumb');
						elem.setAttribute('alt', name);
						elem.src = field;
					}
					else {
						elem = document.createElement('div');
						elem.setAttribute('aria-hidden', true);
						elem.classList.add('mgeo-thumb', 'mgeo-thumb-placeholder');
						elem.innerHTML = field;
					}
				}
				else {
					let text = '';
					if (name) {
						let words = name.split(' ');
						if (words[0]) text += words[0][0];
						if (words[1]) text += words[1][0];
					}

					elem = document.createElement('div');
					elem.setAttribute('aria-hidden', true);
					elem.classList.add('mgeo-thumb', 'mgeo-thumb-placeholder');
					elem.innerHTML = text.toUpperCase();
				}

				return elem;
			}

			this.getItem = function(location) {
				if (!location.id) location = s.getLocation(location);
				if (!location) return false;

				const item = document.createElement('li');
				item.classList.add('mgeo-dir-item');
				item.dataset.location = location.id;
				item.dataset.sort = location.title;

				const a = document.createElement('a');
				item.appendChild(a);
				a.addEventListener('click', function(e) {
					e.preventDefault();
					self.actions.focus(location);
				});

				if (self.o.thumbnail) {
					const thumbEl = self.dir.getThumbnail(location.title, location.res.thumbnail);
					a.appendChild(thumbEl);
				}

				if (location.res.title) {
					const titleEl = document.createElement('h4');
					titleEl.classList.add('mgeo-dir-title');
					titleEl.innerHTML = location.res.title;
					a.appendChild(titleEl);
				}

				if (location.res.about) {
					const aboutEl = document.createElement('span');
					aboutEl.classList.add('mgeo-dir-about');
					aboutEl.innerHTML = location.res.about;
					a.appendChild(aboutEl);
				}

				return item;
			}

			this.registerGroups = function(groups) {
				if (!groups) return false;

				groups.forEach(function(group) { if (group.id) s.g[group.id] = group; });
			}

			this.modifyLocation = function(id = null, obj = null) {
				if (!id) { // add
					let location = s.resolveLocation(obj);
					self.container.addMarker(location);
					self.sidebar?.addLocation(location);
				}
				else if (!obj) { // remove
					self.container.removeMarker(s.l[id]);
					self.sidebar?.removeLocation(id);
					delete s.l[id];
				}
				else { // modify
					if (id == obj.id) { // id same
						Object.assign(s.l[id], obj);
						s.resolveLocation(s.l[id]);
					}
					else { // id changed
						if (s.l[id].res?.marker) s.l[id].res.marker.setId(obj.id);

						s.l[obj.id] = Object.assign(s.l[id], obj);
						s.resolveLocation(s.l[obj.id]);
						delete s.l[id]
					}
					self.sidebar?.removeLocation(id);
					self.sidebar?.addLocation(s.l[obj.id]);
				}
			}

			this.resolveLocations = function(locations, external = false) {
				if (!locations) return false;
				locations.forEach(function(location) {
					s.resolveLocation(location, external);
				});
			}

			this.resolveLocation = function(location, external = false) {
				if (!location.id) return;
				if (external) {
					location.external = true;
					Object.keys(location).forEach(function(attr) { if (location[attr] === '') delete location[attr]; }); // remove empty attributes from external
				}

				let res = location.res ? Object.assign({}, location.res) : {};
				delete location.res;

				if (location.sample && s.l[location.sample]) res = Object.assign({}, s.l[location.sample]);
				else if (!location.sample && s.l[self.o.globalSample]) res = Object.assign({}, s.l[self.o.globalSample]);

				location.res = Object.assign(res, location);
				delete location.res.sample;
				delete location.res.coord;
				location.res.desc = s.replaceVars(location.res.desc, location.res);
				if (!location.res.fill && s.g[location.res.group]?.color) location.res.fill = s.g[location.res.group]?.color;
				if (!location.res.style && s.g[location.res.group]?.style) location.res.style = s.g[location.res.group]?.style;

				location.res.isolated = self.container.l[location.res.layer]?.object.parentLayer || location.res.layer || false;
				if (location.res.action == 'isolate') location.res.isolated = location.id;

				s.l[location.id] = location;

				return s.l[location.id];
			}

			this.getLocation = function(id) {
				return s.l[id];
			}

			this.normalizeString = function(s) {
				if (s) return s.toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, "");
				else return '';
			}

			this.sort = function(dir, order = 1) {
				let items = Array.from(dir.getElementsByClassName('mgeo-dir-item'));

				items.sort(function(a, b) {
					a = a.dataset.sort;
					b = b.dataset.sort;

					if (isNaN(a) || isNaN(b)) return (a < b) ? -1 * order : 1 * order;
					else return (a - b) * order;
				});

				items.forEach(function(item) { dir.appendChild(item); });
			}

			this.sortBy = function(dir, attribute, order = 1) {
				let items = Array.from(dir.getElementsByClassName('mgeo-dir-item'));
				items.forEach(function(item) {
					item.dataset.sort = s.normalizeString(s.l[item.dataset.location][attribute]);
				});
				this.sort(dir, order);
			}	
		}

		// container module
		function Container() {
			const s = this;

			let map;
			let globalMarkers;
			let tooltip;
			let hovertip;
			let overlay;
			let overControl = false;
			let fillZoom;
			let ratio = 1;
			let sidebarParent;
			let hovered = null;
			let extentInteraction = null;
			let markerDrag = null;
			let markerHandle = null;
			let zones = {};
			let levelSelector;
			let fzi = 0; // feature z index
			let extf = {}; // external features
			let nullStyle = new ol.style.Style(null);
			let cache = {
				fill: {},
				stroke: {},
				image: {},
				text: {},
				style: {}
			}

			this.controls = null;
			this.extent = null;

			this.width = 0;
			this.height = 0;

			this.isolation = false;

			this.l = {};
			this.layers = {};

			this.emptyfeatures = 0;

			this.init = function() {
				this.el = document.createElement('div');
				this.el.classList.add('mgeo-container');
				self.el.appendChild(this.el);

				const observer = new ResizeObserver(function(entries) {
					if (s.width != s.el.offsetWidth || s.height != s.el.offsetHeight) s.resize();
				});
				observer.observe(this.el);

				self.o.extent = this.safeExtent(self.o.extent);

				// extent ratio
				ratio = ol.extent.getWidth(self.o.extent)/ol.extent.getHeight(self.o.extent);

				// controls
				this.controls = document.createElement('div');
				this.controls.classList.add('mgeo-controls');

				const controls = new ol.Collection();
				if (self.o.rotation) controls.push(new ol.control.Rotate({ target: this.getControlZone('mgeo-' + self.o.rotation), label: self.styles.getIcon('compass') }));
				if (self.o.fullscreen) controls.push(new ol.control.FullScreen({ target: this.getControlZone('mgeo-' + self.o.fullscreen) }));
				if (self.o.resetButton) controls.push(new ol.control.Control({ target: this.getControlZone('mgeo-' + self.o.resetButton), element: this.resetControl() }));
				if (self.o.zoomButtons) controls.push(new ol.control.Zoom({ target: this.getControlZone('mgeo-' + self.o.zoomButtons) }));
				if (self.o.attribution) controls.push(new ol.control.Attribution({ target: this.getControlZone('mgeo-bottom-right'), collapsible: self.o.attributionCollapsible }));

				// map
				map = new ol.Map({
					target: this.el,
					controls: controls
				});

				map.getInteractions().forEach(function(interaction) { if (interaction instanceof ol.interaction.MouseWheelZoom) map.removeInteraction(interaction); });

				// mousewheel
				if (self.o.mouseWheel) {
					let mousewheelcond = ol.events.condition.always;
					if (self.o.mouseWheelShift) mousewheelcond = ol.events.condition.shiftKeyOnly;

					map.addInteraction(new ol.interaction.MouseWheelZoom({ condition: mousewheelcond }));
				}

				let oc = this.el.getElementsByClassName('ol-overlaycontainer-stopevent')[0];
				oc.appendChild(this.controls);

				// if hovertooltip
				if (self.o.hoverTooltip) this.hoverTooltip();

				// tooltip
				overlay = new ol.Overlay({});
				map.addOverlay(overlay);

				// hovering features
				let hovered = null;
				map.on('pointermove', function(e) {
					const feature = this.forEachFeatureAtPixel(e.pixel, function(feature) { return feature; }, { hitTolerance: 4 });

					if (feature && feature.getId() && !markerDrag) {
						const location = self.dir.l[feature.getId()];

						if (location != hovered) {
							if (hovered != self.actions.focused) s.setLocationStyle(hovered, 'base');
							if (location != self.actions.focused) s.setLocationStyle(location, 'hover');
							hovered = location;
						}
					}
					else if (hovered != self.actions.focused) {
						s.setLocationStyle(hovered, 'base');
						hovered = null;
					}
				});

				// click event
				map.on('singleclick', this.featureClick);

				// cursor pointer
				map.on('pointermove', function(e) {
					const pixel = map.getEventPixel(e.originalEvent);
					const hit = map.hasFeatureAtPixel(pixel);
					map.getTargetElement().style.cursor = hit ? 'pointer' : '';
				});

				return this;
			}

			this.newLayer = function(layer) {
				if (!layer.id) return false;

				let object;
				let style = s.getStyle(layer.style);

				if (!style) {
					style = new ol.style.Style({
						fill: new ol.style.Fill({
							color: '#f4f4f4',
						}),
						stroke: new ol.style.Stroke({
							color: '#bbb',
							width: 0.5,
						})
					});
				}

				switch (layer.type) {
					case 'osm':
						object = new ol.layer.Tile({ source: new ol.source.OSM({ interpolate: true }) });
						break;
					case 'markers':
						object = new ol.layer.Vector({
							updateWhileInteracting: !self.o.improvedPerformance,
							updateWhileAnimating: !self.o.improvedPerformance,
							className: 'mgeo-markers',
							source: new ol.source.Vector({ wrapX: false }),
							renderBuffer: self.o.renderBuffer
						});
						break;
					case 'stamen':
						object = new ol.layer.Tile({ source: new ol.source.Stamen({ layer: layer.source }) });
						break;
					case 'image':
						let extent = layer.extent ? s.safeExtent(layer.extent) : s.safeExtent(self.o.extent);
						object = new ol.layer.Image({
							source: new ol.source.ImageStatic({
								url: layer.source,
								imageExtent: extent
							})
						});
						break;
					case 'xyz':
						object = new ol.layer.Tile({
							source: new ol.source.XYZ({
								url: layer.source
							})
						});
						break;
					case 'mapbox':
						const mapboxStyles = {
							'bright': 'mapbox://styles/mapbox/bright-v9',
							'streets': 'mapbox://styles/mapbox/streets-v11',
							'outdoors': 'mapbox://styles/mapbox/outdoors-v11',
							'light': 'mapbox://styles/mapbox/light-v10',
							'dark': 'mapbox://styles/mapbox/dark-v10',
						}

						object = new ol.layer.MapboxVector({
							styleUrl: mapboxStyles[layer.source],
							accessToken: self.o.mapboxtoken
						});
						break;
					case 'bingmaps':
						object = new ol.layer.Tile({
							preload: Infinity,
							source: new ol.source.BingMaps({
								key: self.o.bingmapskey,
								hidpi: true,
								imagerySet: layer.source
							})
						});
						break;
					case 'maptiler':
						const maptilerStyles = {
							'basic': 'https://api.maptiler.com/maps/basic/{z}/{x}/{y}@2x.png?key=',
							'satellite-v2': 'https://api.maptiler.com/tiles/satellite-v2/{z}/{x}/{y}.jpg?key=',
							'bright': 'https://api.maptiler.com/maps/bright/{z}/{x}/{y}@2x.png?key=',
							'pastel': 'https://api.maptiler.com/maps/pastel/{z}/{x}/{y}@2x.png?key=',
							'hybrid': 'https://api.maptiler.com/maps/hybrid/{z}/{x}/{y}@2x.jpg?key=',
							'streets': 'https://api.maptiler.com/maps/streets/{z}/{x}/{y}@2x.png?key=',
							'topo': 'https://api.maptiler.com/maps/topo/{z}/{x}/{y}@2x.png?key=',
							'voyager': 'https://api.maptiler.com/maps/voyager/{z}/{x}/{y}@2x.png?key='
						}

						object = new ol.layer.Tile({
							source: new ol.source.XYZ({
								attributions: '<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> ' + '<a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>',
								url: maptilerStyles[layer.source] +  self.o.maptilerkey,
								tileSize: 512,
								tilePixelRatio: 2
							})
						});
						break;
					case 'geojson':
					case 'topojson':
					case 'builtin':
						let format;
						if (layer.type == 'geojson') format = new ol.format.GeoJSON();
						else if (layer.type == 'topojson' || layer.type == 'builtin') format = new ol.format.TopoJSON();

						let source = new ol.source.Vector({
							attributions: '&copy; <a href="https://www.mapplic.com" target="_blank">Mapplic GEO</a>',
							url: (layer.type == 'builtin') ? self.localize.pdir + 'maps/' + layer.source : layer.source,
							format: format,
							wrapX: false,
							overlaps: false
						});

						source.on('addfeature', function(e) {
							let id = e.feature.getId();
							if (id) {
								extf[id] = e.feature;

								let location = self.dir.l[id];
								if (location) {
									location.res.feature = e.feature;
									location.res.offset = [0, 0];
									location.res.layer = layer.id;

									if (!location.res.layer) location.res.layer = layer.id;
									s.setLocationStyle(location);
								}
								else s.emptyfeatures++;
							}
						});

						object = new ol.layer.Vector({
							updateWhileInteracting: !self.o.improvedPerformance,
							updateWhileAnimating: !self.o.improvedPerformance,
							renderBuffer: self.o.renderBuffer,
							source: source,
							style: style
						});
						break;
					case 'svg':
						object = new ol.layer.Layer({ className: 'different-layer', render: function() { return null; } });

						var svgContainer = document.createElement('div');
						var xhr = new XMLHttpRequest();
						xhr.open('GET', layer.source);
						xhr.addEventListener('load', function () {
							var svg = xhr.responseXML.documentElement;
							svgContainer.ownerDocument.importNode(svg);
							svgContainer.appendChild(svg);

							svgContainer.style.width = svg.getAttribute('width');
							svgContainer.style.height = svg.getAttribute('height');
							svgContainer.style.transformOrigin = 'top left';
							svgContainer.className = 'mgeo-svg-layer';

							var width = parseFloat(svgContainer.style.width);
							var height = parseFloat(svgContainer.style.height);
							var svgExtent = s.safeExtent(layer.extent);
							var svgWidth = ol.extent.getWidth(svgExtent);
							var svgCenter = ol.extent.getCenter(svgExtent);
							var svgResolution = svgWidth / width;

							object.render = function (frameState) {
								var scale = svgResolution / frameState.viewState.resolution;
								var center = frameState.viewState.center;
								var size = frameState.size;
								var cssTransform = ol.transform.composeCssTransform(
									size[0] / 2,
									size[1] / 2,
									scale,
									scale,
									frameState.viewState.rotation,
									(svgCenter[0] - center[0]) / svgResolution - width / 2,
									(center[1] - svgCenter[1]) / svgResolution - height / 2
								);
								svgContainer.style.transform = cssTransform;
								svgContainer.style.opacity = this.getOpacity();
								return svgContainer;
							}

							object.setExtent(svgExtent);
						});
						xhr.send();
						break;
					default:
						return false;
				}

				if (layer.extent) object.setExtent(this.safeExtent(layer.extent));
				if (layer.minZoom) object.setMinZoom(layer.minZoom);
				if (layer.maxZoom) object.setMaxZoom(layer.maxZoom);

				return object;
			}

			this.addLayers = function(layers) {
				this.layers = layers;

				if (!layers) layers = [];
				if (layers.length < 1) layers.push({'id': 'example-layer', 'title': 'Example Layer', 'type': 'osm'});

				if (self.o.levelselector) map.addControl(new ol.control.Control({ target: this.getControlZone('mgeo-' + self.o.levelselector), element: this.levelSelectorControl(layers) }));

				for (const layer of layers) {
					if (layer.disabled === true) continue;
					if (layer.id) s.l[layer.id] = layer;

					let object;

					if (layer.layers) {
						// layer group
						const c = new ol.Collection();
						for (const sub of layer.layers) {
							if (sub.disabled === true) continue;
							if (sub.id) s.l[sub.id] = sub;
							sub.object = this.newLayer(sub);
							sub.object.parentLayer = layer.id;
							c.push(sub.object);
						}
						object = new ol.layer.Group({ layers: c });
					}
					// single layer
					else object = this.newLayer(layer);

					if (object) {
						map.addLayer(object);
						layer.object = object;

						if (layer.hide) object.setVisible(false);
						if (layer.opacity) object.setOpacity(parseFloat(layer.opacity));
					}
				}

				// global markers
				globalMarkers = this.newLayer({ id: 'global', type: 'markers'});
				map.addLayer(globalMarkers);
			}

			this.safeExtent = function(extent) {
				if (!extent) return false;
				if (Array.isArray(extent)) return extent;
				else return extent.split(',').map(Number);
			}

			this.fit = function(extent, options) {
				map.getView().fit(this.safeExtent(extent), options);
			}

			// drag&drop markers
			this.startMarkerDrag = function(id, field = null) {
				const location = self.dir.l[id];
				if (!location) return false;

				let marker = location.res.marker;
				if (!marker) marker = this.getMarkerHandle(location);

				const c = new ol.Collection();
				c.push(marker, true);

				if (markerDrag) this.stopMarkerDrag(markerDrag);
				markerDrag = new ol.interaction.Modify({ features: c });
				map.addInteraction(markerDrag);
				markerDrag.on('modifyend', function(e) {
					const coord = marker.getGeometry().getCoordinates();
					const lonlat = ol.proj.toLonLat(coord);

					location.res.coord = coord;
					if (field) field.value = lonlat[1].toFixed(6) + ',' + lonlat[0].toFixed(6);
				});
			}

			this.stopMarkerDrag = function() {
				if (markerDrag) {
					map.removeInteraction(markerDrag);
					markerDrag = null;
					this.hideMarkerHandle();
				}
			}

			this.getMarkerHandle = function(location) {
				if (!markerHandle) {
					markerHandle = new ol.Feature({ geometry: new ol.geom.Point(s.resolvedPosition(location)) });
					globalMarkers.getSource().addFeature(markerHandle);
				}
				else {
					markerHandle.getGeometry().setCoordinates(s.resolvedPosition(location));
					markerHandle.setStyle(null);
				}
				return markerHandle;
			}

			this.hideMarkerHandle = function() {
				if (!markerHandle) return false;
				markerHandle.setStyle(new ol.style.Style({}));
			}

			// edit extent
			this.editExtent = function(field) {
				if (!field) this.removeExtentInteraction();

				let extent = field.value.split(',').map(Number);

				if (extentInteraction) {
					map.removeInteraction(extentInteraction);
					extentInteraction = null;
					this.mapView();
					this.resetZoom(0);
				}
				else {
					extentInteraction = s.addExtentInteraction(extent);

					this.mapView([-18878672.443631265, -9326678.110352932, 21196344.241947223, 19298333.807917416], 1, 20);
					map.getView().fit(self.o.extent, {duration: 600, padding: [50, 50, 50, 50]});

					extentInteraction.on('extentchanged', function(e) {
						field.value = extentInteraction.getExtent();
					});
				}
			}

			this.addExtentInteraction = function(extent) {
				const extentStyle = new ol.style.Style({
					stroke: new ol.style.Stroke({
						color: 'rgba(100, 100, 100, 0.4)',
						lineJoin: 'miter',
						width: 3
					})
				});

				const extentInteraction = new ol.interaction.Extent({
					condition: ol.events.condition.shiftKeyOnly,
					boxStyle: extentStyle,
					extent: extent
				});
				map.addInteraction(extentInteraction);

				return extentInteraction;
			}

			this.removeExtentInteraction = function() {
				map.removeInteraction(extentInteraction);
				extentInteraction = null;
			}

			this.mapView = function(extent = self.o.extent, minZoom = self.o.minZoom, maxZoom = self.o.maxZoom, zoom = 1, center = null, duration = 0) {
				if (extent) this.extent = extent;
				if (!center) center = ol.extent.getCenter(extent);

				let view = new ol.View({
					projection: 'EPSG:3857',
					center: center,
					enableRotation: self.o.rotation,
					rotation: self.o.rotate,
					extent: extent,
					minZoom: minZoom,
					maxZoom: maxZoom,
					constrainOnlyCenter: true,
					padding: self.o.padding,
					showFullExtent: self.o.showFullExtent,
					zoom: zoom,
					constrainResolution: self.o.constrainResolution
				});
				map.setView(view);
				if (duration >= 0) view.fit(this.extent, {duration: duration});
				
				fillZoom = Math.floor(view.getZoomForResolution(view.getResolutionForExtent(extent)));
				view.setMinZoom(fillZoom);

				view.on('change:resolution', function(e) {
					if (!fillZoom) return false;
					if (self.o.blurOnZoomOut && view.getZoom() <= fillZoom) self.actions.blur();
				});
				map.updateSize();

				self.el.style.height = 'auto';

				return view;
			}

			this.resize = function() {
				let view = map.getView();
				let sw = 0; // width of sidebar

				if (self.o.sidebar) {
					if (self.el.offsetWidth < self.o.portrait) {
						if (self.o.portraitSidebarAbove) self.el.insertBefore(self.sidebar.el, self.el.firstChild);
						else self.el.appendChild(self.sidebar.el);
						self.el.classList.add('mgeo-portrait');
					}
					else {
						sidebarParent.appendChild(self.sidebar.el);
						self.el.classList.remove('mgeo-portrait');
						sw = self.sidebar.el.offsetWidth;
					}

					if (self.o.rightSidebar) {
						self.o.padding[1] = sw;
						this.controls.style.right = sw + 'px';
					}
					else {
						self.o.padding[3] = sw;
						this.controls.style.left = sw + 'px';
					}
				}

				let h = (this.el.offsetWidth - sw) / ratio; // auto height
				h = Math.min(Math.max(h, self.o.minHeight), self.o.maxHeight);
				self.container.el.style.height = h + 'px';

				map.updateSize();

				s.width = s.el.offsetWidth;
				s.height = s.el.offsetHeight;

				this.mapView(s.extent || self.o.extent, self.o.minZoom, self.o.maxZoom, view.getZoom());
			}

			// controls
			this.getControlZone = function(name) {
				if (zones[name]) return zones[name];
				else {
					const el = document.createElement('div');
					el.classList.add('mgeo-control-zone', name);
					this.controls.appendChild(el);
					zones[name] = el;

					return zones[name];
				}
			}

			this.addSidebarControl = function(el) {
				let sidebar = new ol.control.Control({element: el});
				map.addControl(sidebar);

				sidebarParent = el.parentElement;

				el.addEventListener('mouseenter', function() { overControl = true; });
				el.addEventListener('mouseleave', function() { overControl = false; });
			}

			this.resetControl = function() {
				const button = document.createElement('button');
				button.addEventListener('click', function(e) {
					e.preventDefault();
					s.resetIsolation();
					s.resetZoom();
					self.actions.blur();

					self.el.dispatchEvent(new CustomEvent('mapreset'));
				});
				button.appendChild(self.styles.getIcon('reset'));

				const element = document.createElement('div');
				element.classList.add('ol-reset', 'ol-unselectable', 'ol-control');
				element.appendChild(button);

				return element;
			}

			// levels
			this.levelSelectorControl = function(layers) {
				levelSelector = document.createElement('div');
				levelSelector.classList.add('mgeo-level-switcher', 'ol-unselectable', 'ol-control');
				
				for (const layer of layers) {
					if (!layer.disabled && layer.selectable) {
						const button = document.createElement('button');
						button.setAttribute('data-level', layer.id);
						button.innerHTML = layer.title;
						if (!layer.hide) button.classList.add('mgeo-selected');
						button.addEventListener('click', function(e) {
							e.preventDefault();
							s.switchLevel(layer.id);
						});

						levelSelector.appendChild(button);
					}
				}
				return levelSelector;
			}

			this.switchLevel = function(target) {
				// hide
				const selected = levelSelector?.getElementsByClassName('mgeo-selected')[0];
				if (selected) {
					selected.classList.remove('mgeo-selected');
					s.l[selected.getAttribute('data-level')].object.setVisible(false);
				}

				// show
				s.l[target].object.setVisible(true);
				levelSelector?.querySelector('button[data-level="' + target + '"]')?.classList.add('mgeo-selected');
			}

			this.resetZoom = function(duration = 600) {
				map.getView().fit(this.extent, {duration: duration});
			}

			this.resolvedPosition = function(location) {
				if (!location.res) location.res = {};
				if (!location.res.coord) {
					if (location.coord) {
						let coord = location.coord.split(',');
						location.res.coord = ol.proj.fromLonLat([coord[1], coord[0]]);
					}
					else if (location.res.feature) location.res.coord = ol.extent.getCenter(location.res.feature.getGeometry().getExtent());
					else return false;
				}

				return location.res.coord;
			}

			this.focusLocation = function(location) {
				this.setLocationStyle(location, 'active');
			}
			
			this.blurLocation = function(location) {
				if (location.res?.feature) location.res.feature.highlight = false;
				this.setLocationStyle(location, 'base');
			}

			this.highlightLocation = function(location) {
				if (location.res?.marker) location.res.marker.setStyle(this.getMarkerStyle(location, 'base'));
				else if (location.res?.feature && location.res?.style) {
					location.res.feature.setStyle(this.getStyle(location.res.style, 'hover', location.res.label));
					location.res.feature.highlight = true;
				}
			}

			this.disableLocation = function(location) {
				if (location.res?.marker) location.res.marker.setStyle(nullStyle);
				else if (location.res?.feature && location.res?.style) {
					location.res.feature.setStyle(this.getStyle(location.res.style, 'base', location.res.label))
					location.res.feature.highlight = false;
				}
			}

			// layer isolation
			this.applyIsolation = function(location) {
				if (!location) {
					this.resetIsolation(-1);
					return false;
				}
				if (!this.l[location.id]?.object) return false;
				if (this.isolation) {
					if (this.isolation.tid == location.id) return false;
				}

				let initial = this.isolation.parent,
					pid = this.l[location.res.layer].object.parentLayer || location.res.layer;

				this.isolation = {
					tid: location.id,
					target: this.l[location.id]?.object,
					pid: pid,
					parent: this.l[pid]?.object
				}
				if (initial) this.isolation.initial = initial;
				else this.isolation.initial = this.isolation.parent;

				this.isolation.target.setVisible(true);
				this.isolation.parent.setVisible(false);

				setTimeout(function() {
					let view = map.getView();
					s.mapView(s.safeExtent(s.l[location.id].extent) || location.res.feature.getGeometry().getExtent(), s.l[location.id].minZoom, s.l[location.id].maxZoom, view.getZoom(), view.getCenter(), -1);
				}, 500);
			}

			this.resetIsolation = function(duration = 400) {
				if (!this.isolation) return false;

				this.isolation.target.setVisible(false);
				this.isolation.initial.setVisible(true);
				this.isolation = false;

				let view = map.getView();
				this.mapView(self.o.extent, self.o.minZoom, self.o.maxZoom, view.getZoom(), view.getCenter(), duration);
			}

			// hover tooltip
			this.hoverTooltip = function() {
				hovertip = document.createElement('div');
				hovertip.classList.add('mgeo-tooltip', 'mgeo-hover-tooltip');
				this.el.appendChild(hovertip);

				const title = document.createElement('h4');
				title.classList.add('mgeo-title');
				hovertip.appendChild(title);

				const overlay = new ol.Overlay({
					element: hovertip
				});

				map.addOverlay(overlay);

				map.on('pointermove', function(e) {
					const feature = this.forEachFeatureAtPixel(e.pixel, function(feature) { return feature; }, { hitTolerance: 4 });

					if (feature && feature.getId() && !overControl) {
						const location = self.dir.l[feature.getId()];

						if (location && location.res.title && location != self.actions.focused) {
							location.res.feature = feature;

							let coord = e.coordinate;
							if (!self.o.hoverTooltipFollow) coord = s.resolvedPosition(location);

							title.innerHTML = location.res.title;

							overlay.setPosition(coord);
							if (location.res.offset) overlay.setOffset(location.res.offset);
							hovertip.style.left = Math.round(-hovertip.offsetWidth/2) + 'px';
							hovertip.style.opacity = 1;
						}
						else hovertip.style.opacity = 0;
					}
					else hovertip.style.opacity = 0;
				});
			}

			this.featureClick = function(e) {
				const feature = this.forEachFeatureAtPixel(e.pixel, function(feature) { return feature; }, { hitTolerance: 4 });

				if (feature) {
					self.el.dispatchEvent(new CustomEvent('featureclick', { detail: { feature: feature, coord: e.coordinate } }));

					let location = self.dir.getLocation(feature.getId());
					if (location) {
						location.res.feature = feature;
						self.actions.focus(location);
					}
				}
			}

			this.zoomTo = function(location, pt = 0) {
				let duration = 500,
					padding = [pt, 0, 0, 0],
					geometry = location?.res.feature?.getGeometry(),
					extent = false;

				if (location.res.layer) {
					const i = location.res.isolated || location.res.layer;

					const isolate = self.dir.l[i]?.action == 'isolate' ? self.dir.l[i] : false;
					this.applyIsolation(isolate);

					if (!isolate) this.switchLevel(self.container.l[location.res.layer].object.parentLayer || location.res.layer);
					else extent = this.safeExtent(self.container.l[isolate.id].extent);
				}

				const view = map.getView();

				if (geometry && geometry.getType() != 'Point') view.fit(extent || geometry.getExtent(), { duration: duration, padding: padding });
				else {
					let zoom = (location && location.res.zoom) || Math.min(self.o.maxZoom, Math.round(view.getZoom() * 2));
					let respos = s.resolvedPosition(location);

					if (zoom >= 0 && respos) {
						view.fit(new ol.geom.Point(respos), {
							padding: padding,
							maxZoom: zoom,
							duration: duration
						});
					}
				}
			}

			// tooltip
			this.addTooltip = function(location, small = false) {
				if (!location) return false;
				if (hovertip) hovertip.style.opacity = 0;

				tooltip = document.createElement('div');
				tooltip.classList.add('mgeo-tooltip', 'mgeo-popup');
				tooltip.dataset.location = location.id;
				if (location.res.group) tooltip.dataset.group = location.res.group;
				this.el.appendChild(tooltip);

				let container = document.createElement('div');
				container.classList.add('mgeo-popup-container');

				let close = document.createElement('button');
				close.classList.add('mgeo-close');
				close.appendChild(self.styles.getIcon('cross'));
				close.addEventListener('click', function(e) {
					e.preventDefault();
					self.actions.blur();
					return false;
				});
				container.appendChild(close);

				if (location.res.title) {
					let title = document.createElement('h4');
					title.classList.add('mgeo-title');
					title.innerHTML = location.res.title;
					container.appendChild(title);
				}

				if (!small) {
					if (location.res.image) {
						let ic = document.createElement('div');
						ic.classList.add('mgeo-tooltip-image');
						
						let image = document.createElement('img');
						image.src = location.res.image;
						ic.appendChild(image);
						tooltip.prepend(ic);
					}

					if (location.res.about) {
						let about = document.createElement('h5');
						about.classList.add('mgeo-about');
						about.innerHTML = location.res.about;
						container.appendChild(about);
					}

					if (location.res.desc) {
						let content = document.createElement('div');
						content.classList.add('mgeo-popup-content');
						content.innerHTML = location.res.desc;
						container.appendChild(content);
					}

					let buttons = document.createElement('div');
					buttons.classList.add('mgeo-buttons');
					container.appendChild(buttons);
					
					if (location.res.link) {
						let more = document.createElement('a');
						more.classList.add('mgeo-more');
						more.href = location.res.link;
						more.innerHTML = self.localize.more;
						if (location.res.fill) more.style.backgroundColor = location.res.fill;
						buttons.appendChild(more);
					}
				}
				else tooltip.classList.add('mgeo-tooltip-small');

				tooltip.appendChild(container);

				overlay.setElement(tooltip);
				overlay.setPosition(s.resolvedPosition(location));
				if (location.res.offset) overlay.setOffset(location.res.offset);
				tooltip.style.left = Math.round(-tooltip.offsetWidth/2) + 'px';
				tooltip.style.opacity = 1;

				tooltip.addEventListener('mouseenter', function() { overControl = true; });
				tooltip.addEventListener('mouseleave', function() { overControl = false; });

				if (!small) self.el.dispatchEvent(new CustomEvent('popuprender', { detail: tooltip }));

				return tooltip;
			}

			this.hideTooltip = function() {
				let tooltip = overlay.getElement();
				if (!tooltip) return false;

				tooltip.style.opacity = 0;
				tooltip.style.pointerEvents = 'none';
			}

			// style cache
			this.getFill = function(color) {
				if (!color) return undefined;
				if (!cache.fill[color]) cache.fill[color] = new ol.style.Fill({ color: color });
				return cache.fill[color];
			}

			this.getStroke = function(color, width = 1) {
				if (!color) return undefined;
				let key = Array.prototype.join.call(arguments, '-');
				if (!cache.stroke[key]) cache.stroke[key] = new ol.style.Stroke({ color: color, width: width });
				return cache.stroke[key];
			}

			this.getText = function(text, fill = '#fff', font = 'bold 10px sans-serif', offsetY = 2) {
				if (!text) return undefined;
				let key = Array.prototype.join.call(arguments, '-');
				if (!cache.text[key]) cache.text[key] = new ol.style.Text({ text: text, fill: this.getFill(fill), font: font, offsetY: offsetY });
				return cache.text[key];
			}

			this.getStyle = function(name, state = 'base', text = false) {
				let key = Array.prototype.join.call(arguments, '-');

				if (cache.style[key]) return cache.style[key];
				else {
					const style = self.styles.s[name];
					if (!style) return false;
					else {
						cache.style[key] = new ol.style.Style({
							fill: this.getFill(style[state]?.fill || style['base']?.fill),
							stroke: this.getStroke(style[state]?.stroke || style['base']?.stroke, style['base'].strokeWidth),
							text: this.getText(text, style['base'].textColor || '#888', '11px sans-serif'),
							zIndex: fzi++
						});
						return cache.style[key];
					}
				}
			}

			this.setLocationStyle = function(location, state = 'base') {
				if (!location) return false;

				if (location.res?.marker) location.res.marker.setStyle(this.getMarkerStyle(location, state));
				else if (location.res?.feature) {
					if (location.res.style) {
						const text = location.res.type == 'text' ? location.res.label || location.res.title : false;
						if (location.res.feature.highlight && state == 'base') state = 'hover';
						location.res.feature.setStyle(this.getStyle(location.res.style, state, text));
					}
					else if (location.res.fill) location.res.feature.setStyle(new ol.style.Style({ fill: this.getFill(location.res.fill), stroke: this.getStroke('#aaa', 1) }));
				}
			}

			this.markers = {
				'pin': {'src': self.localize.pdir + '/markers/pin.svg', 'size': [21, 28], 'anchor': [0.5, 1], 'textY': -16 },
				'pin1': {'src': self.localize.pdir + '/markers/pin1.svg', 'size': [20, 26], 'anchor': [0.5, 1], 'textY': -16 },
				'pin2': {'src': self.localize.pdir + '/markers/pin2.svg', 'size': [22, 26], 'anchor': [0.5, 1], 'textY': -14 },
				'pin3': {'src': self.localize.pdir + '/markers/pin3.svg', 'size': [16, 24], 'anchor': [0.5, 1], 'textY': -14 },
				'round1': {'src': self.localize.pdir + '/markers/round1.svg', 'size': [20, 20], 'anchor': [0.5, 0.5], 'textY': 2 },
			}

			this.getMarkerStyle = function(location, state = 'base') {
				let style,
					base = self.styles.s[location.res.style]?.['base'],
					fill = self.styles.s[location.res.style]?.[state]?.fill || base?.fill || location.res?.fill,
					textColor = base?.textColor;

				const m = this.markers[location.res.type];
				const type = m ? 'marker' : location.res.type; 

				switch (type) {
					case 'hidden':
						return false;
					case 'marker':
						location.res.offset = [0, -m.size[1] * m.anchor[1] * Math.min(location.res.scale || 1, 1)];
						style = new ol.style.Style({
							image: new ol.style.Icon({
								anchor: m.anchor,
								src: m.src,
								scale: Math.min(location.res.scale || 1, 1),
								color: fill
							}),
							text: this.getText(location.res.label, textColor, 'bold 10px sans-serif', m.textY)
						});
						break;
					case 'text':
						style = new ol.style.Style({ text: this.getText(location.res?.label, textColor, 'bold 12px sans-serif') });
						break;
					case 'thumb':
						if (!location.res.thumbnail) break;
						location.res.offset = [0, -20];
						style = new ol.style.Style({
							image: new ol.style.Icon({
								src: location.res.thumbnail,
								scale: location.res.scale || 1,
								color: fill
							})
						});
						break;
					case 'square':
						location.res.offset = [0, -(location.res.scale * 10 || 10)];
						style = new ol.style.Style({
							image: new ol.style.RegularShape({
								fill: this.getFill(fill),
								stroke: this.getStroke(self.styles.s[location.res.style]?.['base'].stroke || null, self.styles.s[location.res.style]?.['base'].strokeWidth),
								radius: -location.res.offset[1] / 0.785,
								angle: 0.785,
								points: 4
							}),
							text: this.getText(location.res?.label, textColor)
						});
						break;
					case 'dot-l':
					case 'dot-r':
						location.res.offset = [0, -(location.res.scale * 5 || 4)];
						let text = this.getText(location.res?.label, textColor || '#000', '12px sans-serif', 1);
						if (text) {
							if (type == 'dot-r') {
								text.setTextAlign('right');
								text.setOffsetX(location.res.offset[1]-4);
							}
							else {
								text.setTextAlign('left');
								text.setOffsetX(-location.res.offset[1]+4);								
							}
							text.setBackgroundFill(this.getFill('rgba(255,255,255,0.01)'));
						}
						style = new ol.style.Style({
							image: new ol.style.Circle({
								fill: this.getFill(fill),
								stroke: this.getStroke(self.styles.s[location.res.style]?.['base'].stroke || null, self.styles.s[location.res.style]?.['base'].strokeWidth),
								radius: -location.res.offset[1]
							}),
							text: text
						});
						break;
					case 'circle':
					default:
						location.res.offset = [0, -(location.res.scale * 10 || 10)];
						style = new ol.style.Style({
							image: new ol.style.Circle({
								fill: this.getFill(fill),
								stroke: this.getStroke(self.styles.s[location.res.style]?.['base'].stroke || null, self.styles.s[location.res.style]?.['base'].strokeWidth),
								radius: -location.res.offset[1]
							}),
							text: this.getText(location.res?.label, textColor)
						});
				}

				if (state == 'active') style.setZIndex(fzi++);

				return style;
			}

			// markers
			this.addMarkers = function(locations) {
				if (!locations) return false;
				locations.forEach(function(location) { if (!location.res.disabled && location.id != location.sample) self.container.addMarker(location); });
			}

			this.addMarker = function(location) {
				const res = s.resolvedPosition(location);

				if (!location || !location.res.type || location.res.type == 'text') return false;

				const f = new ol.Feature({ geometry: new ol.geom.Point(res) });

				let style = this.getMarkerStyle(location);
				if (!style) return false;
				style.setZIndex(fzi++);

				f.setStyle(style);
				f.setId(location.id);

				location.res.marker = f;

				const layer = s.l[location?.res?.layer]?.object?.getSource() || globalMarkers.getSource();
				layer.addFeature(f);
			}

			this.removeMarker = function(location) {
				const layer = s.l[location?.res?.layer]?.object?.getSource() || globalMarkers.getSource();
				layer.removeFeature(location.res.marker)
			}

			this.getCoord = function(coord = map.getView().getCenter()) {
				const pair = ol.proj.toLonLat(coord);
				return pair[1].toFixed(6) + ',' + pair[0].toFixed(6);
			}
		}
	}

	document.querySelectorAll('.mgeo-map').forEach(function(el) {
		let geo = new MapplicGeo(el);
		geo.init();
	});

})();

Youez - 2016 - github.com/yon3zu
LinuXploit