Chú ý: Sau khi lưu thay đổi trang, bạn phải xóa bộ nhớ đệm của trình duyệt để nhìn thấy các thay đổi. Google Chrome, Firefox, Internet ExplorerSafari: Giữ phím ⇧ Shift và nhấn nút Reload/Tải lại trên thanh công cụ của trình duyệt. Để biết chi tiết và hướng dẫn cho các trình duyệt khác, xem Trợ giúp:Xóa bộ nhớ đệm.

/**
 * Đưa khung bản đồ OpenStreetMap tiếng Việt vào các bài địa danh có thẻ
 * {{coord|display=title}}.
 * 
 * Xem [[Trợ giúp:OpenStreetMap]] và <http://wiki.osm.org/wiki/Vi:Main_Page>.
 */
mw.loader.using("mediawiki.util", function () {

var wpMapDefaults = {
    //* {string} Phiên bản Leaflet
    leafletVersion: "0.4.4",
    //* {number} Chiều cao bản đồ (điểm ảnh)
    height: 350,
    //* {string} Địa chỉ hình tượng ([[Tập tin:WMA button2b.png]])
    iconUrl: "//upload.wikimedia.org/wikipedia/commons/thumb/5/55/WMA_button2b.png/17px-WMA_button2b.png",
    
    /**
     * {string} Định dạng của các địa chỉ hình ảnh bản đồ. Ngoài các placeholder
     * chuẩn của Leaflet, kịch bản này hỗ trợ các placeholder sau:
     * 
     * <dl>
     * <dt>{layer}</dt><dd>tên của lớp bản đồ</dd>
     * </dl>
     * 
     * @see http://leaflet.cloudmade.com/reference.html#url-template
     */
    layerUrlFormat: location.protocol + "//{s}.www.toolserver.org/tiles/{layer}/{z}/{x}/{y}.png",
    
    // Địa phương hóa
    
    //* {string} Mã ngôn ngữ của các địa danh trên bản đồ
    language: mw.config.get("wgContentLanguage"),
    //* {string} Tooltip của hình tượng hiện/ẩn bản đổ
    iconTooltip: "Xem vị trí này trên bản đồ tương tác",
    //* {string} Phím tắt (không bao gồm Ctrl v.v.) hiện/ẩn bản đồ
    iconAccessKey: undefined,   // thí dụ "y"
    
    zoomIn: "Phóng to",
    zoomOut: "Thu nhỏ",
    
    layerBlank: "Bản đồ",
	layerLabels: "Địa danh",
    layerArticles: "Bài viết",
    
    /**
     * {string} Định dạng của lời ghi công những người đóng góp vào
     * OpenStreetMap. Chuỗi có thể có các placeholder sau:
     * 
     * <dl>
     * <dt>{osm}</dt>
     * <dd>liên kết đến #creditOsmUrl có văn bản #creditOsm</dd>
     * <dt>{osmLicense}</dt>
     * <dd>liên kết đến #creditOsmUrlLicenseUrl có văn bản #creditOsmLicense</dd>
     * <dt>{help}</dt>
     * <dd>liên kết đến #helpArticle có văn bản #help</dd>
     * </dl>
     */
    credit: "© những nguời đóng góp vào {osm} ({osmLicense}, {help})",
    
    creditOsm: "OpenStreetMap",
    creditOsmUrl: "//www.openstreetmap.org/",
    creditOsmLicense: "ODbL + CC BY-SA",
    creditOsmLicenseUrl: "//www.openstreetmap.org/copyright",
    
    help: "Trợ giúp",
    helpArticle: "Trợ giúp:OpenStreetMap",
	
	// WIWOSM
	
	layerShapes: "Hình dạng",
    creditShapes: "WIWOSM",
	creditShapesUrl: "//wiki.openstreetmap.org/wiki/WIWOSM?uselang=vi",
    
    // Bản đồ biển xa lộ Mỹ
    
    layerShields: "Biển xa lộ",
    shieldsUrlFormat: "http://elrond.aperiodic.net/mtiles/cutouts/{z}/{x}/{y}.png",
    creditShields: "Shields",
    creditShieldsUrl: "http://elrond.aperiodic.net/shields/",
    
    tunnelUrl: location.protocol + "//toolserver.org/~mxn/poitunnel.html",
};
window.wpMapConfig = $.extend({}, window.wpMapConfig, wpMapDefaults);

/**
 * Tải các kịch bản và bảng kiểu của thư viện bản đồ Leaflet của CloudMade. Hàm
 * này chạy một cách bất đồng bộ.
 * 
 * @param done  {function}  Hàm sẽ được gọi sau khi kịch bản của thư viện được
 *                          tải xong.
 */
function loadLeaflet(done) {
//    var root = "//toolserver.org/~osm/libs/leaflet/" + wpMapConfig.leafletVersion + "/dist/";
    var root = "//cdn.leafletjs.com/leaflet-" + wpMapConfig.leafletVersion + "/";
    mw.loader.load(root + "leaflet.css", "text/css");
    if ($.browser.msie && parseInt($.browser.version, 10) < 9) {
        mw.loader.load(root + "leaflet.ie.css", "text/css");
    }
    $.getScript(root + "leaflet.js").done(done);
}

/**
 * Cho ra định dạng của các địa chỉ hình bản đồ trong lớp được cho vào.
 * 
 * @param name  {string}    Tên của lớp bản đồ.
 * @returns {string}    Định dạng của địa chỉ hình bản đồ.
 */
function layerUrl(name) {
    return wpMapConfig.layerUrlFormat.replace("{layer}", name);
}

/**
 * Phân tích mảng chứa các chuỗi tham số thành một đối tượng tham số.
 * 
 * @param components    {array}     Các thành phần của tọa độ.
 * @param width         {number}    Chiều rộng hiện tại của bản đồ.
 * @returns {object}    Từ điển có tọa độ và các tham số.
 */
function parsedParameters(components, width) {
    //* Phân tích một thành phần của tọa độ.
    var parseCoord = function (pos, neg) {
        var i = 0, coord = 0;
        for (; i < components.length; i++) {
            var component = components[i].toUpperCase();
            if (!component || component == pos || component == neg) {
                if (component == neg) coord *= -1;
                break;
            }
            
            coord += parseFloat(component, 10) / Math.pow(60, i);
        }
        components.splice(0, i + 1);
        return coord;
    };
    
    // Phân tích vĩ độ và kinh độ.
    var lat = parseCoord("N", "S");
    var lng = parseCoord("E", "W");
    
    // Đưa các tham số khác vào từ điển.
    var params = {
        center: new L.LatLng(lat, lng),
    };
    for (var i = 0; i < components.length; i++) {
        var param = components[i].split(":");
        params[param[0]] = param[1];
    }
    
    // Phân tích các tham số tỷ lệ theo [[:en:Template:Coord#type:T]] và
    // [[OpenStreetMap:Zoom levels]].
    var zoomsByType = {
        country: 6, satellite: 6,
        adm1st: 9,
        adm2nd: 11,
        adm3rd: 12, mountain: 12, isle: 12, waterbody: 12, river: 12,
        city: 12,   // city(dân số) là 11–14, tùy dân số
        forest: 13, glacier: 13, event: 13,
        airport: 14,
        edu: 16, pass: 16, railwaystation: 16, landmark: 16,
    };
    var type = params.type ? (params.type.match(/(\w+)(?:\(([\d,]+)\))?/) || []) : [];
    params.zoom = zoomsByType[type[1]] || 11;
    if (type[1] == "city" && type[2]) {
        var pop = params.pop = parseInt(type[2].replace(",", ""), 10);
        if (pop >= 1e7) params.zoom = 11;
        else if (pop >= 1e5) params.zoom = 12;
        else if (pop >= 1e3) params.zoom = 13;
        else params.zoom = 14;
    }
    
    // 1:...
    var scalesByZoom = [5e8, 2.5e8, 1.5e8, 7e7, 3.5e7, 1.5e7, 1e7, 4e6, 2e6, 1e6,
                        1e5, 2.5e5, 1.5e5, 7e4, 3.5e4, 1.5e4, 8e3, 4e3, 2e3];
    if (params.scale) {
        params.scale = parseFloat(params.scale);
        for (var i = scalesByZoom.length - 1; i >= 0; i--) {
            if (params.scale <= scalesByZoom[i]) {
                params.zoom = i;
                break;
            }
        }
    }
    
    // Mét / điểm ảnh
    var mppByZoom = [156412, 78206, 39103, 19551, 9776, 4888, 2444, 1222,
                     610.984, 305.492, 152.746, 76.373, 38.187, 19.093, 9.547,
                     4.773, 2.387, 1.193, 0.596];
    if (params.dim) {
        var dim = parseFloat(params.dim, 10);
        var unit = params.dim.indexOf("km") < 0 ? 1 : 1e3;
        dim *= unit;
        for (var i = mppByZoom.length - 1; i >= 0; i--) {
            if (dim <= mppByZoom[i] * wpMapConfig.height) {
                params.zoom = i;
                break;
            }
        }
        if (width) {
            for (var i = mppByZoom.length - 1; i >= 0; i--) {
                if (dim <= mppByZoom[i] * width) {
                    params.zoom = Math.round((params.zoom + i) / 2.);
                    break;
                }
            }
        }
    }
    
    // [[en:ISO 3166-1 alpha-2]]
    if (params.region) params.country = params.region.match(/^\w\w(?=-)/);
    
    return params;
}
//window.parsedParameters = parsedParameters;                                     // debug

var earthRadius = 6378137 /* m */;
/**
 * Chuyển đổi các điểm EPSG:3857 (Mercator hình cầu) thành tọa độ.
 */
function unprojectLayerPoints(points) {
	if (typeof(points[0]) !== "number") {
		for (var i = 0; i < points.length; i++) {
			unprojectLayerPoints(points[i]);
		}
		return;
	}
	
	var pt = new L.Point(points[0], points[1]).divideBy(earthRadius);
	var coord = L.Projection.SphericalMercator.unproject(pt);
	points[0] = coord.lng;
	points[1] = coord.lat;
}

/**
 * Tạo ra đối tượng bản đồ và thiết lập nội dung.
 * 
 * @param id        {string}    ID của phần tử sẽ trở thành bản đồ.
 * @param params    {object}    Từ điển các tham số của bản đồ.
 * @returns {object}    Bản đồ Leaflet.
 */
function createMap(id, params) {
    // Tải các lớp OpenStreetMap.
    var osmAttrib = wpMapConfig.credit.replace("{osm}", "<a id='openstreetmap-credit' href='" +
                                               wpMapConfig.creditOsmUrl + "'>" + wpMapConfig.creditOsm + "</a>")
                                      .replace("{osmLicense}", "<a href='" + wpMapConfig.creditOsmLicenseUrl +
                                               "'>" + wpMapConfig.creditOsmLicense + "</a>")
                                      .replace("{help}", "<a href='" + mw.util.getUrl(wpMapConfig.helpArticle) +
                                               "'>" + wpMapConfig.help + "</a>");
    var blank = new L.TileLayer(layerUrl("osm-no-labels"), {
        attribution: osmAttrib,
    });
	var shapes = new L.GeoJSON(undefined, {
		style: function (feature) {
			return {
				opacity: 0.5,
				fillColor: "white",
				clickable: false,
			};
		},
	});
    var labels = new L.TileLayer(layerUrl("osm-labels-" + wpMapConfig.language));
    labels.on("tileerror", function (evt) {
        // Nếu lớp chưa có vị trí nào đó, thay thế bằng hình bản đồ đa ngôn ngữ.
        var layerName = "osm-labels-" + wpMapConfig.language;
        if (evt.url.indexOf(layerName) < 0) return;
        evt.tile.src = evt.url.replace(layerName, "osm");
    });
    var articles = new L.LayerGroup();
    
    // <a href='/wiki/Wikipedia:Quyền_tác_giả'>Wikipedia</a>
    
    // Tạo bản đồ.
    var map = new L.Map(id, {
        center: params.center,
        zoom: params.zoom,
        layers: [blank, shapes, labels, articles],
    });
    
    // Việt hóa các điều khiển.
    map.attributionControl.setPrefix("<a href='http://leaflet.cloudmade.com'>Leaflet</a>");
    $(".leaflet-control-zoom-in").attr("title", wpMapConfig.zoomIn);
    $(".leaflet-control-zoom-out").attr("title", wpMapConfig.zoomOut);
    
    // Tạo điều khiển để bật/tắt các lớp.
    var bases = {};
    bases[wpMapConfig.layerBlank] = blank;
    var overlays = {};
    overlays[wpMapConfig.layerLabels] = labels;
    
    // Thiết lập lớp biển xa lộ Mỹ.
    if (["US", "CA", "MX"].indexOf(params.country) >= 0) {
        var shieldsAttrib = ("<a id='shields-credit' href='" +
                             wpMapConfig.creditShieldsUrl + "'>" +
                             wpMapConfig.creditShields + "</a>");
        var shields = new L.TileLayer(wpMapConfig.shieldsUrlFormat, {
            attribution: shieldsAttrib,
        });
        overlays[wpMapConfig.layerShields] = shields;
    }
    
    overlays[wpMapConfig.layerShapes] = shapes;
    overlays[wpMapConfig.layerArticles] = articles;
    map.addControl(new L.Control.Layers(bases, overlays));
    
    // Liên kết đến vị trí hiện tại trên trang chủ OpenStreetMap.
    var updateOsmLink = function (link) {
        if (!link.length) return;
        var center = map.getCenter();
        var base = link.attr("href").match(/^[^?]+/);
        link.attr("href", base + "?lat=" + center.lat + "&lon=" + center.lng +
                  "&zoom=" + map.getZoom());
    };
    var updateOsmLinks = function () {
        updateOsmLink($("#openstreetmap-credit"));
        updateOsmLink($("#shields-credit"));
    };
    updateOsmLinks();
    map.on("moveend", updateOsmLinks);
    
    // Thiết lập lớp ghim và lớp hình dạng.
    var tunnel = $("#poi-tunnel");
    if (!tunnel.length) {
        tunnel = $("<iframe id='poi-tunnel' src='" + wpMapConfig.tunnelUrl + "'></iframe>");
        $(document.body).after(tunnel);
        tunnel.hide();
    }
	var receiveArticles = function (pois) {
        if (pois.length === undefined) return;
        articles.clearLayers();
        for (var i = 0; i < pois.length; i++) {
            if (!pois[i].visible) continue;
            var marker = new L.CircleMarker(new L.LatLng(pois[i].lat, pois[i].lon), {
                color: "red",
                opacity: 0.4,
                fillOpacity: 0.1,
                radius: 5,
            });
            articles.addLayer(marker);
            var content = mw.html.element("a", {
                href: mw.util.getUrl(pois[i].name),
                title: pois[i].name,
            }, pois[i].name);
            marker.bindPopup(content);
        }
	};
	var articleTitle = mw.config.get("wgTitle");
	var receiveShapes = function (json) {
        shapes.clearLayers();
		
		if (json.coordinates) unprojectLayerPoints(json.coordinates);
		else if (json.geometries) {
			for (var i = 0; i < json.geometries.length; i++) {
				unprojectLayerPoints(json.geometries[i].coordinates);
			}
		}
		
		if (shapes.addData(json) && json.type !== "Point") {
			var zoom = map.getBoundsZoom(shapes.getBounds());
			if (zoom > 1) map.fitBounds(shapes.getBounds());
			//else {
			//	var centerHemisphere = params.center.lng >= 0 ? 1 : -1;
			//	shapes.eachLayer(function (layer) {
			//		// Nếu hình đa giác cùng bán cầu với điểm trung tâm, thì
			//		// không cần di chuyển hình đa giác.
			//		var layerPt = (layer instanceof L.Marker ? layer.getLatLng() :
			//					   layer.getBounds().getCenter());
			//		var hemisphere = layerPt.lng >= 0 ? 1 : -1;
			//		if (centerHemisphere < 0 && hemisphere < 0) return;
			//		if (centerHemisphere > 0 && hemisphere > 0) return;
			//		
			//		if (layer instanceof L.Marker) {
			//			layer.setLatLng(layerPt.lat,
			//							layerPt.lng + 360 * centerHemisphere);
			//			return;
			//		}
			//		
			//		var coords = layer.getLatLngs();
			//		for (var i = 0; i < coords.length; i++) {
			//			coords[i].lng += 360 * centerHemisphere;
			//		}
			//		layer._latlngs = coords;	// tránh _convertLatLngs()
			//		layer.redraw();
			//	});
			//	zoom = map.getBoundsZoom(shapes.getBounds());
			//	if (zoom > 1) map.fitBounds(shapes.getBounds());
			//}
		}
	};
    addEventListener("message", function (evt) {
        if (evt.origin !== location.protocol + "//toolserver.org") return;
		
		var data = evt.data;
		if (data.length !== undefined) receiveArticles(data);	// poitunnel cũ
		else if (data.subject === "pois") receiveArticles(data.data);
		else if (data.subject === "shape") receiveShapes(data.data);
    }, false);
    var requestShapes = function () {
        tunnel[0].contentWindow.postMessage({
            subject: "shape",
			lang: wpMapConfig.language,
			article: articleTitle,
        }, wpMapConfig.tunnelUrl);
    };
    tunnel.load(requestShapes);
    var requestArticles = function () {
        tunnel[0].contentWindow.postMessage({
            subject: "pois",
			lang: wpMapConfig.language,
			bbox: map.getBounds().toBBoxString(),
        }, wpMapConfig.tunnelUrl);
    };
    tunnel.load(requestArticles);
    map.on("moveend", requestArticles);
    map.on("layeradd", function (evt) {
        if (evt.layer !== articles) return;
		requestArticles();
		map.on("moveend", requestArticles);
    });
    map.on("layerremove", function (evt) {
        if (evt.layer === articles) map.off("moveend", requestArticles);
    });
    
    // Đặt ghim vào vị trí của chủ đề bài.
    var marker = new L.Marker(params.center);
    map.addLayer(marker);
    var content = mw.html.element("strong", {}, articleTitle);
    marker.bindPopup(content);
    
    return map;
}

/**
 * Cài đặt khung bản đồ và hình tượng hiện/ẩn nó và gỡ bỏ WikiMiniAtlas.
 */
function installMap() {
    // Tìm tọa độ trong liên kết của GeoHack.
    var link = $("#coordinates a[href*='geohack']:not([href*='_globe:'])");
    var params = link && link.attr("href");
    params = params && params.match(/[?&]params=(.+?)(?:&|$)/);
    params = params && params[1].split("_");
    if (!params) return;
    
    // Tạo khung đựng bản đồ.
    $("#contentSub").append("<div id='openstreetmap-container' style='clear: both; display: none;'><div id='openstreetmap' style='height: " +
                            wpMapConfig.height + "px; width: 100%;'></div></div>");
    
    // Vô hiệu WikiMiniAtlas và xóa hình tượng của nó nếu bản đồ đã tải.
    $("#coordinates img").unbind("click").remove();
    wma_settings = {enabled: false};
    
    // Tạo hình tượng địa cầu.
    var icon = $("<a id='openstreetmap-icon' href='#'><img src='" +
                wpMapConfig.iconUrl + "' /></a>");
    
    // Đặt tooltip và phím tắt nếu có.
    var tooltip = wpMapConfig.iconTooltip;
    if (wpMapConfig.iconAccessKey) {
        tooltip += " [" + wpMapConfig.iconAccessKey + "]";
        icon.attr("accesskey", wpMapConfig.iconAccessKey);
    }
    icon.attr("title", tooltip);
    if (wpMapConfig.iconAccessKey) mw.util.updateTooltipAccessKeys(icon);
    
    // Khi nhấn chuột vào hình tượng, mở/đóng bản đồ.
    icon.click(function (evt) {
        // Nếu bản đồ đã được thiết lập, chỉ việc hiện/ẩn.
        var container = $("#openstreetmap-container");
        if (!container || container.hasClass("openstreetmap-loaded")) {
            container.slideToggle("slow", function () {
                this.map.setView(this.mapParams.center, this.mapParams.zoom);
            });
            return;
        }
        container.addClass("openstreetmap-loaded");
        
        loadLeaflet(function () {
            // Thiết lập và hiển thị bản đồ.
            container.slideDown("slow", function () {
                this.mapParams = parsedParameters(params, $(this).width());
//                console.log(this.mapParams);                                    // debug
                this.map = createMap("openstreetmap", this.mapParams);
            });
        });
    });
    
    // Chèn hình tượng đằng trước liên kết tọa độ.
    link.before(icon);
    link.before(" ");
};

$(installMap);

});