MediaWiki:WikiLeaflet.js

Материал из свободной русской энциклопедии «Традиция»
Перейти к: навигация, поиск

Замечание: Чтобы после сохранения вступили в силу изменения стилей, перезагрузите файл //traditio.wiki/w/load.php?debug=false&lang=ru&modules=site&only=styles&skin=vector&*, если используете скин Vector, или //traditio.wiki/w/load.php?debug=false&lang=ru&modules=site&only=styles&skin=common&*, если используете скин Common.

Чтобы вступили в силу изменения скриптов, перезагрузите файл //traditio.wiki/w/load.php?debug=false&lang=ru&modules=site&only=scripts&skin=vector&*, если используете скин Vector, или //traditio.wiki/w/load.php?debug=false&lang=ru&modules=site&only=scripts&skin=common&*, если используете скин Common.

Гаджеты и импортируемые скрипты загружаются отдельными файлами.

/*
 *   Скрипт WikiLeaflet версии 1.0
 *		Требования: jQuery 1.8 и LeafLet 0.7.3.
 *		Для обновления скрипта с сервера перезагрузить
 *		/w/index.php?title=MediaWiki%3AWikiLeaflet.js&action=raw&ctype=text/javascript&dontcountme=s&v=1.0
 */

/*
 * Точка входа. Вызывается по AJAX из MediaWiki:Common.js:
 */
function wlRender ($obj) {
	//														V-- по умолчанию.	V-- во избежание повторной обработки.
	var $map_containers = $obj ? $obj.find ('.wikileaf:not(.leaflet-container)') : $('.wikileaf:not(.leaflet-container)');
	// Обработка каждой карты:
	if ($map_containers &&  $map_containers.each) {
		$map_containers.each (function () {
			var map_settings = wf$2settings ($(this), wlRules);
			attachMap (this, map_settings);
		});
	}
} // -- function wlRender ();

/*
 * Настройки преобразований объектов вики-страницы в объекты карты:
 */
// Синтаксический сахар для конфигурации. Нужно определить перед wlRules:
// Возможность упорядочивать вызов функций:
Function.prototype.at = function (/* int */ order) {
	this.order = order;
    return this;
};
function __url (/* string */ def) {
	return {__type: 'url', __default: def || ''};
}
var __float  = {__type: 'real', __default: null};
var __int    = {__type: 'integer', __default: null};
var __float0 = {__type: 'real', __default: 0};

var wlRules = {
	/*
	 * Настройки карты в целом.
	 */

	// Центр карты:
	wlCenter : {lat: __float, lon: __float, zoom: __int}
	
	// Пределы карты:
  , wlBounds : {south: __float0, west: __float0, north: __float0, east: __float0, flag: false}

	// Высота карты:
  , wlHeight : 500

	// Начальный тайловый сервер:
  , wlTiles : function ($obj) {
  		var str = wf$2atomic ($obj, 'string', 'osm');
		return $.type (wlTileServers [str]				  )	!== 'undefined'	? str
			 : $.type (wlTileServers [wlTileAliases [str]]) !== 'undefined' ? wlTileAliases [str]
																			: wlTileAliases ['osm'];
	} // -- wlTiles

	// Насильственное задание режима редактирования:
  , wlEditable : $('textarea').length > 0
  
	// 	Правила кластеризации:
  , wlCluster : {distance: 0, separator: ', ', iconType: 'default'}

	/*
	 * Элементы карты.	
	 */

	// Типы отметок:
  ,	wlMarker : {
		__index : 'iconID' // -- будет создан ассоциативный массив.
	  ,	iconID	: 'default'
		// Характеристики значка:
	  , iconURL   : __url (LeafletRoot + 'images/marker.png')
	  , iconWidth : 25, iconHeight : 41
		// Характеристики тени:
	  , shadowURL : __url (LeafletRoot + 'trans.gif')
	  , shadowWidth	: function ($obj) {
			// В этой функции и далее до конца wlMarker, this -- формируемый объект настроек wlMarker.
			return wf$2atomic ($obj, 'integer', this.shadowURL === LeafletRoot + 'trans.gif' ? 41 : 1);
		}
	  , shadowHeight : function ($obj) {
			return wf$2atomic ($obj, 'integer', this.shadowURL === LeafletRoot + 'trans.gif' ? 41 : 1);
		}
		// Характеристики якоря:
	  , anchorWidth	: function ($obj) {
			return wf$2atomic ($obj, 'integer', Math.floor (this.iconWidth / 2));
		}
	  , anchorHeight : function ($obj) {
			return wf$2atomic ($obj, 'integer', this.iconHeight);
		}
		// Характеристики всплывающего окна:
	  , popupWidth : 0
	  , popupHeight	: function ($obj) {
			return wf$2atomic ($obj, 'integer', -this.anchorHeight + 2);
		}.at (1) // -- выполнение после предыдущих функций.
		// Характеристики текстовой метки:
	  , labelShiftWidth : function ($obj) {
			return wf$2atomic ($obj, 'integer', this.iconWidth - this.anchorWidth);
		}.at (1) // -- выполнение после предыдущих функций.
	  , labelShiftHeight :  0
	  , labelText : ''
	  , wlCategories : wlItems // -- список через ;.
	  , clusteredIconType : 'default' // -- значок для кластеризованной точки.
	  , clusterRating : 0 // -- эта точка захватит кластер, если у неё будет больше рейтинг.
	} // -- wlMarkers.

	// Отметки:
  , wlPoint : [{ // -- будет создан массив.
		lat: __float, lon: __float, alt: __float0
	  , content: '', iconType: 'default'
	  , labelText: '', labelShiftWidth: 0, labelShiftHeight: 0
	  , wlCategories : wlItems // -- список через ;.
	  , clusteredIconType : 'default' // -- значок для кластеризованной точки.
	  , clusterRating : 0 // -- эта точка захватит кластер, если у неё будет больше рейтинг.
	}] // -- wlPoint.

	// GeoJSON:
  , wlGeoJSON : [{ // -- будет создан массив.
		json : ''
	  , content: ''
	  , wlCategories : wlItems // -- список через ;.
	}] // -- GeoJSON.

	// Фильтры:
  , wlFilterList : false
}; // -- wlRules.

/*
 * Функции разбора настроек карты:
 */

// Функция, находящая в $context объекты .selector и преобразующая их правилом settings в настройки карты.
//		Указатель this -- формируемый родительский объект:
/* any */ function wf$find (/* jQuery */ $context, /* string */ selector, /* any */ settings) {
	wlStripParagraphs ($context);
	return wf$2settings.call (this, $context.children ('.' + selector), settings);
} // -- /* any */ function wf$find (/* jQuery */ $context, /* string */ selector, /* any */ settings)

// Функция, преобразующая $obj в настройки карты правилом settings.
//		Указатель this -- формируемый родительский объект:
/* any */ function wf$2settings (/* jQuery */ $obj, /* any */ settings) {
	// Преобразование инструкций по разбору:
	var processed_settings = wfType (settings);
	var type     = processed_settings.type;
	var defaults = processed_settings.defaults;
	
	var parent = this;
	var $source = processed_settings.source ($obj);
	
	var result;
	switch (type) {
		case 'function':
			// 	В this функции передаётся формируемый объект настроек:
			return defaults.call (parent, $source);
		case 'object':
			result = wf$2objects.call (parent, $source, defaults);
			// В зависимости от того, что нужно вышестоящему объекту, вернуть один объект или несколько:
			return (this.__type === 'assoc' || this.__type === 'array') ? result : result [result.length - 1];
		case 'array':
		case 'assoc':
			return wf$2list.call (parent, $source, defaults, processed_settings.initial (parent), processed_settings.appender);
		// Атомарные типы:
		case 'boolean':
			return defaults || $source.length;
		case 'real':
		case 'integer':
		case 'string':
		case 'url':
			return wf$2atomic.call (parent, $source, type, defaults);		
	} // -- switch (type)
} // -- /* any */ function wf$2settings (/* jQuery */ $obj, /* any */ settings)

// Функции для преобразования атомарных значений:
/* atomic */ function wf$2atomic (/* jQuery */ $obj, /* string или function */ caster, /* atomic */ def) {
	// Обработка значения в зависимости от требуемого типа:
	var processor = $.isFunction (caster) ? caster : cast [caster];
	var html = $.trim ($obj.html ());
	return processor (html, def);
} // -- /* atomic */ function wf$2atomic (/* jQuery */ $obj, /* string или function */ caster, /* atomic */ def)
var cast = {
	real : function (/* string */ html, /* atomic */ def) {
		var result = html.replace (',', '.');
		return result.match (/^-?\d+(\.\d+)?$/) ? parseFloat (result) : def;
	}
  , integer : function (/* string */ html, /* atomic */ def) {
		return html.match (/^-?\d+$/) ? parseInt (html, 10) : def;
	}	
  , 'string' : function (/* string */ html, /* atomic */ def) {
		return html ? html : def;
	}	
  , url : function (/* string */ html, /* atomic */ def) {
		// Переписать регулярное выражение:
		var img_path = '(thumb\\/)?[01-9a-zA-Z]{1}\\/[01-9a-zA-Z]{2}(\\/[01-9a-zA-Z_.()-]+\\.([Gg][Ii][Ff]|[Pp][Nn][Gg]|[Jj][Pp][Ee][Gg])){1,2}'
		var components = (new RegExp ('^(' + ImageRoot + ')?(' + img_path + ')$')).exec (html);
		return components ? (components [1] || ImageRoot) + components [2] : def;
	} // -- url : function (/* string */ html, /* atomic */ def)
};
// -- функции для преобразования атомарных значений.

// Получение объекта настроек -- записи.
//		$obj      -- объект jQuery, содержащий нужный узел DOM,
//		structure -- структура объекта.
//		Возвращается всегда массив, хотя бы из одного элемента:
/* array */ function wf$2objects (/* jQuery */ $obj, /* object */ structure) {
	var defaults = {__type: 'object'};
	var delayed = [];
	var processed;
	$.each (structure, function (/* string */ field, /* any */ rule) {
		if ($.isFunction (rule)) {
			// Правило обработки -- функция. Все они с отложенным вызовом:
			var order = rule.order || 0;
			delayed [order] = delayed [order] || {};
			delayed [order] [field] = rule;
		} else {
			// Рекурсивный вызов немедленно. Нужно также передать this:
			processed = wf$find.call (defaults, $obj, field, rule);
		} // -- if ($.isFunction (rule))
		defaults [field] = processed;
	}); // -- $.each (structure, function (/* string */ field, /* any */ rule)
	var result;
	result = [defaults];
	// Отложенный вызов функций:
	$.each (result, function (i, current) {
		$.each (delayed, function (j, order) {
			if (order) {
				$.each (order, function (/* string */ field, /* function */ func) {
					// Функция должна получить указатель на объект в this:
					current [field] = wf$find.call (current, $obj, field, func);
				}); // -- $.each (order, function (/* string */ field, /* function */ func
			} // -- if (order)
		}); // -- $.each (delayed, function (j, order)
	}); // -- $.each (result, function (i, current)
	// Удаление объектов, в которых не заполнены обязательные поля:
	var final = [];
	$.each (result, function () {
		var complete = true;
		$.each (this, function (field, value) {
			if (value === null) {
				return complete = false;
			}
		}); // -- $.each (this, function (field, value)
		if (complete) final.push (this);
	}); // -- $.each (result, function ()
	return final;
} // -- /* array */ function wf$2objects (/* jQuery */ $obj, /* object */ structure)
	
// Получение массива или ассоциативного массива.
//		$obj 	  -- объект jQuery с уже отобранными нужными узлами DOM,
//		structure -- правила формирования элемента массива,
//		initial   -- начальное значение списка
//		appender  -- функция, добавляющая простой массив к простому или ассоциативному массиву:
/* array или object */ function wf$2list (/* jQuery */ $objects, /* object */ structure, /* array or assoc */ initial, /* function */ appender) {
	var result = initial; // -- инициализация.
	var context = this;
	$objects.each (function (i, dom) {
		// 	Рекурсивный вызов:
		var current = wf$2settings.call (result, $(dom), structure);
		if (current && !$.isArray (current)) {
			current = [current];
		}
		appender (result, current);
	});
	return result;
} // -- /* array или object */ function wf$2list (/* jQuery */ $objects, /* object */ structure, /* function */ appender)

// Получение типа из настроек:
/* string */ function wfType (/* object */ settings) {
	var ret = {
		defaults : settings // -- глубокая копия.
	  , type	 : $.type (settings)
	  , source   : function ($obj) { return $obj.last (); }
	};
	switch (ret.type) {
		case 'object':
			ret.defaults = $.extend (true, {}, ret.defaults); // -- глубокая копия.
			if ($.type (settings.__type) === 'string') {
				ret.type = settings.__type;
				ret.defaults = settings.__default;
			} else if ($.type (settings.__index) !== 'undefined') {
				// Ассоциативный массив:
				ret.type = 'assoc';
				var index = ret.defaults.__index;
				delete ret.defaults.__index;
				// Исходное значение:
				ret.initial = ret.defaults [index]
							? function (/* object */ parent) {
								  var ini = wf$2settings.call (parent, $(''), ret.defaults);
								  if ($.isArray (ini)) {
									  ini = ini [0];
								  }
								  var ini_folded = {__type: 'assoc'};
								  ini_folded [ret.defaults [index]] = ini;
								  return ini_folded;
						  } : function () { return {__type: 'assoc'}; };
				// Добавлятель:
				ret.appender = function (/* object */ append2, /* array */ appended) {
					$.each (appended || {}, function (i, current) {
						append2 [current [index]] = current;
					});
				};
				// Неуникальные значения:
				ret.source = function ($obj) { return $obj; };
			} // -- if ($.type (settings.__type) === 'string')
			break;
		case 'array':
			// Простой массив:
			ret.defaults = settings [0];			
			ret.initial = function () {
				var r = ret.defaults ? [] : [ret.defaults];
				r.__type = 'array';
				return r;
			};
			ret.appender = function (/* object */ append2, /* array */ appended) {
				$.merge ((append2 || []), (appended || []));
			};
			// Неуникальные значения:
			ret.source = function ($obj) { return $obj; };
			break;
		case 'number':
			ret.type = Math.floor (settings) !== settings ? 'real' : 'integer'; break;
	} // -- switch ($.type (settings))
	return ret;
} // -- /* string */ function wfType (/* object */ settings)

// Служебная функция для удаления подчинённых абзацев:
function wlStripParagraphs (/* jQuery */ $object) {
	$object.children ('p').each (function () {
		var $this = $(this);
		$this.replaceWith ($this.contents ());
	});
} // -- function wlStripParagraphs (/* jQuery */ $object).

// Функция, режущая список в массив по ;:
/* array */ function wlItems (/* jQuery */ $obj) {
	var html = wf$2atomic ($obj, 'string', '');
	return html ? (html.split (/\s*;\s*/) || []) : [];
}

/*
 * Функции, выводящие карту:
 */
 
// Прикрепляет к объекту div карту на основании разобранных параметров settings:
/* L.Map */ function attachMap (/* DOM */ div, /* object */ settings) {

	var $div = $(div);
	
	// Максимальная ширина всплывалки:
	var popupMaxWidth = Math.floor (0.8 * $div.width ());
	
	// Установка высоты и очистка карты:
	$div.css ('height', settings.wlHeight + 'px').html ('');

	// Типы значков:
	var markers = {};
	var labels  = {};
	$.each (settings.wlMarker, function (id, marker) {
		if (id.substring (0, 2) === '__') return true; // -- пропуск __type.
		// Создание типа значка:
		var markerClass = new L.Icon ({//L.Icon.extend ({
			iconUrl     : marker.iconURL
		  , shadowUrl   : marker.shadowURL
		  , iconSize    : new L.Point (marker.iconWidth  , marker.iconHeight)
		  , shadowSize  : new L.Point (marker.shadowWidth, marker.shadowHeight)
		  , iconAnchor  : new L.Point (marker.anchorWidth, marker.anchorHeight)
		  , popupAnchor : new L.Point (marker.popupWidth , marker.popupHeight)
		});
		markers [id] = markerClass; //new markerClass ();
		// Создание типа текстовой метки:
		labels  [id] = {
			textLabel 		 : marker.textLabel
		  , labelShiftWidth  : marker.labelShiftWidth
		  , labelShiftHeight : marker.labelShiftHeight
		  , wlCategories     : marker.wlCategories
		};
	}); // -- $.each (settings.wlMarker, function (id, marker).

	// Наследование меток и категорий от типа значка:
	var inheritAll = function (/* array */ points, /* object */ markers, /* boolean */ filters) {
		$.each (points, function () {
			var point = this;
			var iconType = markers [point.iconType] || markers ['default'];
			// Служебная функция-замыкание для экономии кода:
			var inherit = function (/* string */ attr) {
				var own = point [attr];
				point [attr] = !own || $.isArray (own) && own.length === 0 ? iconType [attr] : own;
			} // -- var inherit = function (/* string */ attr, /* any */ own)
			if (filters) {
				inherit ('wlCategories');
			}
			inherit ('labelText');
			inherit ('clusteredIconType');
			inherit ('clusterRating');
			if (point.labelText !== '') {
				// Настройки для местоположения метки:
				inherit ('labelShiftWidth');
				inherit ('labelShiftHeight');			
			} // -- if (this.labelText !== '').
		}); // -- $.each (settings.wlPoint, function ().
	}; // -- var inheritAll = function (/* array */ points, /* object */ markers, /* boolean */ filters)

	// Служебные функции:
	var min = function (a, b) {
		return a < b || b === null ? a : b;
	}
	var max = function (a, b) {
		return a > b || b === null ? a : b;
	}
	var union = function (a, b) {
		if (!a) return b;
		if (!b) return a;
		return {
			south: min (a.south, b.south)
		  , north: max (a.north, b.north)
		  , west : min (a.west,  b.west)
		  , east : max (a.east,  b.east)
		};
	} // -- var union = function (a, b)
	
	// Препроцессинг точек и GeoJSON -- всё, что можно сделать, не зная зума:
	var preprocessPoints = /* object */ function (/* array */ points) {
		var bounds = null;
		$.each (points, function (i, point) {
			bounds = union (bounds, {south: point.lat, west: point.lon, north: point.lat, east: point.lon});
		});
		return {points: points, bounds: bounds};
	} // -- var preprocessPoints = /* object */ function (/* array */ points, /* object */ center)
	
	var preprocessGeoJSON = function (/* array */ jsons) {
		var bounds = null;
		var processed_jsons  = [];
		$.each (jsons, function (i, json) {
			var parsedJSON = null;
			try {
				parsedJSON = $.parseJSON (json.json);
			} catch (e) {}
			if (!parsedJSON) return true; // -- пропустить итерацию.
			var gjLayer = new L.GeoJSON (parsedJSON, {
				onEachFeature: function (/* GeoJSON */ feature, /* ILayer */ layer) {
					// Добавление стиля изнутри GeoJSON:
					if (feature.properties.style && feature.layer.setStyle) {
						feature.layer.setStyle (feature.properties.style);
					}
					// Попапы для частей GeoJSON:
					if (feature.properties && feature.properties.description) {
						layer.bindPopup (feature.properties.description);
					}
				} // -- onEachFeature: function (/* GeoJSON */ feature, /* ILayer */ layer)
			}); // -- var gjLayer = new L.GeoJSON (parsedJSON, ...
			gjLayer.wlCategories = json.wlCategories;
			gjLayer.content = json.content;
			processed_jsons.push (gjLayer);
			// Обновление крайних точек:			
			var bc = gjLayer.getBounds ();
			bounds = union (bounds, {
				south: bc.getSouth (), north: bc.getNorth (), west: bc.getWest (), east: bc.getEast ()
			});
		}); // -- $.each (jsons, function (i, json)
		return {jsons: processed_jsons, bounds: bounds};
	} // -- var preprocessGeoJSON = function (/* array */ jsons)
					
	// Асинхронное размещение объектов на карте:
	
	// Служебная функция-замыкание, общая для значков, помет и GeoJSON:
	var place = function (/* function */ constructor, /* object */ object_settings, /* DOM */ popup, /* L.Map */ m) {
		// Создание объекта:
		var obj = constructor (object_settings);
		if (!obj) return;

		// Получение категорий:
		var categories = settings.wlFilterList && object_settings.wlCategories.length > 0
					   ? object_settings.wlCategories
					   : null;
		// Внесение в категории, если надо:
		if (categories) {
			// Объект в категориях:
			m.wlCategories = m.wlCategories || {};
			$.each (categories, function (i, category) {
				m.wlCategories [category] = m.wlCategories [category] || L.layerGroup ().addTo (m);
				// Добавление к категории глубокой копии объекта,
				//		на случай, если он в нескольких категориях:
				var clone = $.extend (true, {}, obj);
				// Подключение всплывающего HTML:
				if (popup) {
					clone.bindPopup (popup, {maxWidth: popupMaxWidth});
				}				
				m.wlCategories [category].addLayer (clone);
			}); // -- $.each (categories, function (i, category)
		} else {
			// Некатегоризированный объект:
			// Подключение всплывающего HTML:
			if (popup) {
				obj.bindPopup (popup, {maxWidth: popupMaxWidth});
			}			
			m.addLayer (obj);
		} // -- if (categories)
		return obj;
	}; // -- var place = function (/* function */ constructor, /* object */ object_settings, /* DOM */ popup, /* L.Map */ m)
	
	// Маркеры:
	var addMarkers = function (/* L.Map */ m, /* array */ points) {
		$.each (points, function (i, point) {
			// Маркер:
			place (
				function (/* object */ point_settings) {
					marker = new L.Marker (
						new L.LatLng (point_settings.lat, point_settings.lon)
					  , {icon: markers [point_settings.iconType] || markers ['default']}
					);
					return marker;
				} // -- function (/* object */ point_settings)
			  , point
			  , point.content
			  , m
			); // -- place (...)
		}); // -- $.each (points, function (i, point)
	}; // -- var addMarkers = function (/* L.Map */ m, /* array */ points)
	
	// Метки:
	var addLabels = function (/* L.Map */ m, /* array */ labels) {
		$.each (labels, function () {
			var label = this;		
			// Текстовая пометка, если надо:
			if (label.labelText) {
				place (
					function (/* object */ label_settings) {
						return new L.LabelOverlay (
							new L.LatLng (label_settings.lat, label_settings.lon)
						  , label_settings.labelText
						  , {offset: new L.Point (label_settings.labelShiftWidth, label_settings.labelShiftHeight)}
						);
					} // -- function (/* object */ label_settings)
				  , label
				  , null
				  , m
				); // -- place (...)
			} // -- if (label.labelText !== '')
		}); // -- $.each (labels, function ().
	}; // -- var addLabels = function (/* L.Map */ m, /* array */ labels)

	// Размещение GeoJSON на карте:
	var addGeoJSON = /* array */ function (/* L.Map */ m, /* array */ jsons) {
		$.each (jsons, function (i, json) {
			place (
				function (/* object */ processed_json) {
					return processed_json;
				} // -- function (/* object */ processed_json)
			  , json
			  , json.content
			  , m
			); // -- place (...)
		}); // -- $.each (jsons, function (i, json)
	}; // -- var addGeoJSON = function (/* L.Map */ m, /* array */ jsons)
	
	// Кластеризация:
	var clusterise = function (/* L.Map */ m, /* array */ points, /* int */ d, /* string */ sep, /* string */ icon) {
		var d2 = Math.pow (d, 2);
		var clustered = [];
		
		// Служебная функция, возвращающая ссылку на подъодящий кластер (null, если нет):
		var findCluster = /* int */ function (/* L.Point */ p, /* L.Map */ m, /* array */ clusters, /* int */ d2) {
			var found = null;
			$.each (clusters, function (index, cluster) {
				if (cluster) {
					var p2 = m.project (L.latLng (cluster.lat, cluster.lon));
					if (Math.pow (p2.x - p.x, 2) + Math.pow (p2.y - p.y, 2) <= d2) {
						found = index;
						return false; // -- завершение цикла.
					}
				} // -- if (cluster)
			}); // -- $.each (clustered, function (j, cluster)
			return found;
		} // -- var findCluster = function (/* L.Point */ p, /* L.Map */ m, /* array */ clusters, /* int */ d2)
		
		// Служебная функция, соединяющая в кластер точку с точкой или кластер с точкой:
		var mergePoints = /* object */ function (/* object */ p1, /* object */ p2, /* string */ sep, /* string */ icon) {
			// Служебная функция, оборачивающая html в таблицу:
			var add2Table = function (/* string */ tbl, /* string */ html, /* string */ icon_url) {
				var ret = tbl || '<table></table>';
				return ret.substr (0, ret.length - 8)
					 + '<tr><th><img src="' + icon_url + '" /></th>'
					 + '<td>' + html + '</td></tr>'
					 + '</table>';
			} // -- var add2Table = function (/* string */ tbl, /* string */ html, /* string */ icon_url)
			var merged;
			if (!p1.is_cluster) {
				merged = $.extend (true, {is_cluster: true}, p1);
				merged.content = add2Table (null, p1.content, (markers [p1.iconType] || markers ['default']).options.iconUrl);
			} else {
				merged = p1;
			}
			// Слияние всплывающих окон:
			// Добавление новой строки к таблице:
			merged.content = add2Table (merged.content, p2.content, (markers [p2.iconType] || markers ['default']).options.iconUrl);
			// Выбор значка:
			merged.clusteredIconType = p1.clusteredIconType === p2.clusteredIconType
									|| p1.clusterRating > p2.clusterRating ? p1.clusteredIconType
									 : p2.clusterRating > p1.clusterRating ? p2.clusteredIconType
									 									   : icon;
			merged.iconType = merged.clusteredIconType;
			// Слияние текстовых меток (поглощением или сложением):
			merged.labelText = (p1.labelText || '').indexOf (p2.labelText || '') > -1 ? p1.labelText
							 : (p2.labelText || '').indexOf (p1.labelText || '') > -1 ? p2.labelText
							 : (p1.labelText || '') + sep + (p2.labelText || '');
						 
			// Слияние категорий с удалением дубликатов:
			merged.wlCategories = $.merge (merged.wlCategories, p2.wlCategories).filter (function (a) {
				return !this [a] ? this [a] = true : false;
			}, {});
			return merged;
		} // -- var mergePoints = function (/* object */ p1, /* object */ p2, /* string */ sep, /* string */ icon)
		
		$.each (points, function (i, point) {
			var p1 = m.project (L.latLng (point.lat, point.lon));
			var index = findCluster (p1, m, clustered, d2);
			if (index !== null) {
				var new_cluster = mergePoints (clustered [index], point, sep, icon);
				clustered.push (new_cluster);
				delete clustered [index];
			} else {
				clustered.push (point);
			}
		}); // -- $.each (points, function (i, point)
	
		return clustered.filter (function (elem) { return elem; });
	} // -- var clusterise = function (/* L.Map */ m, /* array */ points, /* int */ d, /* string */ sep, /* string */ icon)
	
	// Показ всего, а также вывод контроля тайлов и категорий.
	//		Вызов событием загрузки/зума:
	var addAll = function (/* L.Map */   m
						 , /* array */   points
						 , /* array */   jsons
						 , /* object */  cluster
						 , /* object */  tiles
						 , /* boolean */ redraw) {
		if (redraw) {
			// Удалить все категории:
			delete m.wlCategories;
			// Удалить все слои:
			m.eachLayer (function (layer) {
				// кроме тайлов:
				if (!(layer instanceof L.TileLayer)) {
					m.removeLayer (layer);
				}
			});
		} // -- if (redraw)

		// Кластеризация, если нужно:
		var points_clustered = cluster.distance
							 ? clusterise (m, points, cluster.distance, cluster.separator, cluster.iconType)
							 : points;

		// Размещение объектов с категоризацией:
		addMarkers (m, points_clustered);
		addLabels  (m, points_clustered);
		addGeoJSON (m, jsons);
		
		// Выбор тайлов и фильтры:
		if (typeof L.control.layers !== 'undefined') {
			if (redraw) {
				if (m.wlLayerControl) {
					m.wlLayerControl.removeFrom (m);
				}
			}
			m.wlLayerControl = L.control.layers (tiles, m.wlCategories).addTo (m);
		}
	}; // -- var addAll = function (...)
	
	// Клонирование тайлов для карты. Без него переключатели нескольких карт на странице будут ломать друг друга:
	var tiles = {};
	$.each (wlTileServers, function (/* string */ name, /* object */ params) {
		tiles [name] = L.tileLayer (params.url, params.options);
	});
	// Создание объекта карты и первоначальные настройки:
	var map = new L.Map (div, {
		scrollWheelZoom: false				// -- зум колесом выключен.
	}).addControl (L.control.scale ()		// -- контроль зума.
	).addLayer (tiles [settings.wlTiles]);	// -- тайлы по умолчанию.
	// Добавление инструментов редактирования:
	if (settings.wlEditable) {
		addEditTools ($div, map, popupMaxWidth);
	}

	// Наследование точками характеристик значков:
	inheritAll (settings.wlPoint, settings.wlMarker, settings.wlFilterList);

	// Инициализация крайних точек:
	// Препроцессинг точек и GeoJSON для выявления центра и границ:
	var preprocessedPoints  = preprocessPoints  (settings.wlPoint);
	var preprocessedGeoJSON = preprocessGeoJSON (settings.wlGeoJSON);
	var bounds = null;
	if (settings.wlBounds.south || settings.wlBounds.west || settings.wlBounds.north || settings.wlBounds.east) {
		// Границы заданы явно:
		bounds = settings.wlBounds;
	} else {
		// Границы рассчитываются по объектам карты:
		bounds = union (settings.wlCenter ? {
			south: settings.wlCenter.lat
		  , north: settings.wlCenter.lat
		  , west : settings.wlCenter.lon
		  , east : settings.wlCenter.lon
		} : null, preprocessedPoints.bounds);
		bounds = union (bounds, preprocessedGeoJSON.bounds);
	} // -- if (settings.wlBounds.south || settings.wlBounds.west || settings.wlBounds.north || settings.wlBounds.east)
	// Если нет ни центра, ни объектов, ни границ:
	// Ph'nglui mglw'nafh Mithgol G'lenjik wgah'nagl fhtagn:
	bounds = bounds || {south: 44.5445, west: 37.9718, north: 44.6054, east: 38.1223};
	// Пределы в формате Leaflet:
	var wlBounds = L.latLngBounds (L.latLng (bounds.south, bounds.west), L.latLng (bounds.north, bounds.east));	
	var center = settings.wlCenter && settings.wlCenter.lat && settings.wlCenter.lon
			   // Центр задан явно:
			   ? {lat: settings.wlCenter.lat, lon: settings.wlCenter.lon}
			   // Центр рассчитан по объектам карты:
			   : {lat: (bounds.south + bounds.north) / 2, lon: (bounds.west + bounds.east) / 2};
			   
	// Начальный зум:
	if (settings.wlCenter && settings.wlCenter.zoom !== null) {
		map.setZoom (settings.wlCenter.zoom); // -- зум задан.
	} else {
		map.fitBounds (wlBounds, {padding: [20, 20]}); // -- автозум.
	}
	// Начальный центр:
	map.setView (L.latLng (center.lat, center.lon));
	// Нужно установить границы на замке:
	if (settings.wlBounds.flag) {
		map.setMaxBounds (wlBounds);
	} // -- if (settings.wlBounds.flag)
	
	// Наконец, размещение объектов карты:
	addAll (map, preprocessedPoints.points, preprocessedGeoJSON.jsons, settings.wlCluster, tiles);
	
	if (settings.wlCluster.distance) {
		// Если работает кластеризация, то надо перерисовывать при зуме:
		map.on ('zoomend', function (/* Event */ event) {
			addAll (event.target, preprocessedPoints.points, preprocessedGeoJSON.jsons, settings.wlCluster, tiles, true);
		});
	} // -- if (settings.wlCluster.distance)
	
	return map;
} // -- /* L.Map */ function attachMap (/* DOM */ div, /* object */ settings).

L.LabelOverlay = L.Class.extend ({/* https://github.com/CloudMade/Leaflet/issues/154 */
   initialize: function (/* LatLng */ latLng, /* String */ label, options) {
      this._latlng = latLng;
      this._label  = label;
      L.Util.setOptions (this, options);
   },
   options: {
      offset: new L.Point (0, 2)
   },
   onAdd: function (map) {
      this._map = map;
      if (!this._container) {
         this._initLayout ();
      }
      map.getPanes ().overlayPane.appendChild (this._container);
      this._container.innerHTML = this._label;
      map.on ('viewreset', this._reset, this);
      this._reset ();
   },
   onRemove: function (map) {
      map.getPanes ().overlayPane.removeChild (this._container);
      map.off ('viewreset', this._reset, this);
   },
   _reset: function () {
      var pos = this._map.latLngToLayerPoint (this._latlng);
      var op = new L.Point (pos.x + this.options.offset.x, pos.y - this.options.offset.y);
      L.DomUtil.setPosition (this._container, op);
   },
   _initLayout: function () {
      this._container = L.DomUtil.create ('div', 'leafletLabel');
   }
}); // -- L.LabelOverlay.

// Список сокращённых идентификаторов допустимых серверов тайлов:
var wlTileAliases = {
	osm			  : 'OpenStreetMap.Mapnik'
/*  , local		  : mw.config.wgServer*/
  , cycle		  : 'OpenCycleMap'
  , mapquest	  : 'MapQuest'
  , openaerial	  : 'Open Aerial'
  , hydda		  : 'Hydda.Full'  
/*  , osmosnimki	  : 'OSMosnimki'
  , kosmosnimki   : 'Космоснимки'*/
  , openmapsurfer : 'OpenMapSurfer Roads'
  , esriwp		  : 'Esri.WorldPhysical'
  , thundertr	  : 'Thunderforest.Transport'
/*  , sea			  : 'OpenSeaMap'*/
}; // -- var wgTileAliases.
// Список допустимых серверов тайлов:
var wlTileServers = {
	'OpenStreetMap.Mapnik' : {
		url: '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
	  , options: {
			minZoom: 0, maxZoom: 18,
			attribution: 'Map data © <a href="//osm.org/">OpenStreetMap</a> contributors'
	}} // -- 'OpenStreetMap.Mapnik'.
  , 'MapQuest' : {
  		url: '//otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png'
	  , options: {
      	minZoom: 0, maxZoom: 18, subdomains: '1234',
      	attribution: 'Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="//developer.mapquest.com/content/osm/mq_logo.png"> — Map data © <a href="http://osm.org/">OpenStreetMap</a> contributors'
	}} // -- 'MapQuest'.
  , 'Open Aerial' : {
  		url: '//otile{s}.mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.jpg'
	  , options: {
	  		minZoom: 0, maxZoom: 11, subdomains: '1234',
			attribution: 'Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="//developer.mapquest.com/content/osm/mq_logo.png"> — Portions Courtesy NASA/JPL-Caltech and U.S. Depart. of Agriculture, Farm Service Agency'
	}} // -- 'Open Aerial'.
	/*
  , 'OSMosnimki' : {
  		url: '//{s}.tile.osmosnimki.ru/kosmo/{z}/{x}/{y}.png'
	  , options: {
	  		minZoom: 0, maxZoom: 17, subdomains: 'abcd',
			attribution: 'Tiles Courtesy of <a href="//kosmosnimki.ru/">Kosmosnimki.Ru</a> — Map data © <a href="//osm.org/">OpenStreetMap</a> contributors'
	}} // -- 'OSMosnimki'.
  , 'Космоснимки' : {
  		url: '//{s}.tile.kosmosnimki.ru/kosmo/{z}/{x}/{y}.jpg'
	  , options: {
		 	minZoom: 0, maxZoom: 18, subdomains: 'abcd',
			attribution: 'Tiles Courtesy of <a href="//kosmosnimki.ru/">Kosmosnimki.Ru</a>'
	}}// -- 'Космоснимки'.*/
  , 'OpenMapSurfer Roads' : {
  		url: '//korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}'
	  , options: {
	  		minZoom: 0, maxZoom: 18,
			attribution: 'Tiles by <a href="//openmapsurfer.uni-hd.de/contact.html">Department of Geography, University of Heidelberg</a> — Map data © <a href="//osm.org/">OpenStreetMap</a> contributors'
	}} // -- 'OpenMapSurfer Roads'.
  , 'Esri.WorldPhysical' : {
  		url: '//server.arcgisonline.com/ArcGIS/rest/services/World_Physical_Map/MapServer/tile/{z}/{y}/{x}'
	  , options: {
	  		attribution: 'Tiles &copy; Esri &mdash; Source: US National Park Service',
		maxZoom: 8
	}} // -- 'Esri.WorldPhysical'
  , 'Hydda.Full' : {
  		url: '//{s}.tile.openstreetmap.se/hydda/full/{z}/{x}/{y}.png'
	  , options: {
	  		minZoom: 0, maxZoom: 18, attribution: 'Tiles courtesy of <a href="//openstreetmap.se/" target="_blank">OpenStreetMap Sweden</a> &mdash; Map data {attribution.OpenStreetMap}'
	}} // -- 'Hydda'
/*	
  , 'OpenSeaMap' : {
  		url: '//tiles.openseamap.org/seamark/{z}/{x}/{y}.png'
	  , options: {
	  		attribution: 'Map data: &copy; <a href="//www.openseamap.org">OpenSeaMap</a> contributors'
	}} // -- 'OpenSeaMap'
*/	
  , 'Thunderforest.Transport' : {
  		url: '//{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png'
	  , options: {
	  		attribution: '&copy; <a href="//www.opencyclemap.org">OpenCycleMap</a>, &copy; <a href="//openstreetmap.org">OpenStreetMap</a> contributors, <a href="//creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
	}} // -- 'Thunderforest.Transport'
  , 'OpenCycleMap' : {
  		url: '//{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png'
	  , options: {
	  		minZoom: 0, maxZoom: 16,
			attribution: 'Map data © <a href="//osm.org/">OpenStreetMap</a> contributors'
	}} // -- 'OpenCycleMap'.	
};
/*
wlTileServers [mw.config.wgServer] = new L.TileLayer (mw.config.wgServer + '/tiles/{z}/{x}/{y}.png', {
		minZoom: 0, maxZoom: 18,
		attribution: 'Map data © OpenStreetMap contributors'
});*/ // -- wlTileServers [mw.config.wgServer] (local).
// -- var wgTileServers.

// Функция, добавляющая к карте инструменты редактирования:
function addEditTools (/* jQuery */ $wikileaf, /* L.Map */ map, /* int */ popupMaxWidth) {
	// 	Уникальный идентификатор карты (хэш):
	var id = 'result_' + $wikileaf.html ().split ('').reduce (function (a, b) {a = ((a << 5) - a) + b.charCodeAt (0); return a & a}, 0);
	$wikileaf.after ('<div id="' + id + '"></div>');
	// 	Привязка события для записи координат щелчка мышью:
	map.on ('click', function (e) {
		var latlngStr = '' + e.latlng.lat.toFixed (6) + '|' + e.latlng.lng.toFixed (6);
		var popup = new L.Popup ({maxWidth: popupMaxWidth});
		popup.setLatLng (e.latlng);
		popup.setContent ('<b>Координаты жмяка мышою:</b><br><code>{{wl|точка|' + latlngStr + '}}</code>');
		map.openPopup (popup);
	}).on ('drag zoomend', function (e) {
		var latlngStr = '' + map.getCenter ().lat.toFixed (6) + '|' + map.getCenter ().lng.toFixed (6);
		$('#' + id).html (
			'<b>Координаты центра карты и увеличение:</b> <code>{{wl|центр|' + latlngStr
		  + '|' + map.getZoom() + '}}</code>'
		  + '<br />'
		  + '<b>Границы карты:</b> <code>{{wl|границы'
		  + '|' + map.getBounds ().getSouth ()
		  + '|' + map.getBounds ().getWest  ()
		  + '|' + map.getBounds ().getNorth ()
		  + '|' + map.getBounds ().getEast  ()
		  + '}}</code>'
		);
	});
} // -- function addEditTools (/* jQuery */ $wikileaf, /* L.map */ map, /* int */ popupMaxWidth).

/*
 * Прочие функции
 */
 
// Отладочная функция:
function showObj (obj, depth) {
    var out = '';
    if (!depth) {
        depth = 2;
    }
    var level = '';
	var type = $.type (obj);
	if (type === 'array' && obj !== null && depth > 1) {
        out += 'array [\n';
		level = level + '    ';
        $.each (obj, function (key, val) {
            out += level + key + ' : ' + showObj (val, depth - 1) + '\n';
        });
        out += ']';
	} else if (type === 'object' && obj !== null && depth > 1) {
        out += 'object (' + obj.constructor.name + ') : {\n';
    	level = level + '    ';		
        $.each (obj, function (key, val) {
            out += level + key + ' : ' + showObj (val, depth - 1) + '\n';
        });
        out += '}';
    } else if (type === 'object' && obj !== null && depth <= 1) {
        out = level + '[object]\n';
    } else if (type === 'string' || type === 'number') {
		out = level + type + ': ' + obj;
	} else if (obj && typeof (obj.toString) !== 'undefined') {
        out = level + type + ': ' + obj.toString ();
    } else {
        out = level + 'null';
    }
    return out;
}