Bước tới nội dung

Mô đun:dialect map

Từ điển mở Wiktionary
local export = {}

local m_links = require("Module:links")
local m_lang = require("Module:languages")

local RADIUS = 0

function export.map_header(text)
	return tostring(
		mw.html.create("h2")
		:wikitext(text)
		:done()
	)
end

function export.get_coords(lat, long, i, n)
	if n > 1 then
		return { long + RADIUS * math.cos(2 * math.pi * i / n), lat + RADIUS * math.sin(2 * math.pi * i / n) }
	else
		return { long, lat }
	end
end

function export.make_point(lat, long, color, term, term_url, location, i, n, group, marker_lang, group_id, term_group, wikitext, legend_wikitext)
	local properties = {
		["term"] = term,
		["loc"] = location,
		["marker-color"] = color,
		["marker-group"] = group,
		["marker-lang"] = marker_lang,
		["group-id"] = group_id,
		["term-group"] = term_group,
		["wikitext"] = wikitext,
	}
	if term_url then
		properties["url"] = term_url
	end
	if legend_wikitext then
		properties["legendWikitext"] = legend_wikitext
	end
	return {
		type = "Feature",
		properties = properties,
		geometry = {
			type = "Point",
			coordinates = export.get_coords(lat, long, i, n)
		}
	}
end

function export.make_polygon(text, color, points)
	local coords = {}
	for _, pt in ipairs(points) do
		table.insert(coords, { pt.long, pt.lat })
	end
	table.insert(coords, { points[1].long, points[1].lat })
	return {
		type = "Feature",
		properties = {
			title = text,
			["stroke-width"] = 0,
			["fill"] = color,
			["fill-opacity"] = 0.2,
		},
		geometry = {
			type = "Polygon",
			coordinates = { coords },
		}
	}
end

function export.make_map(frame, geojson, config)
	return tostring(
		mw.html.create("div")
		:addClass("thumb")
		:addClass("dialect-map__container")
		:wikitext(frame:extensionTag("mapframe", mw.text.jsonEncode(geojson), {
			class = "dialect-map__map",
			mapstyle = config.mapstyle or "osm-intl",
			width = config.width or "100%",
			height = config.height or 600,
			latitude = config.lat,
			longitude = config.long,
			zoom = config.zoom
		}))
		:done()
	)
end

function export.show(frame)
	local geojson = {}

	local params = {
		[1] = { required = false, sublist = true },
	}
	local args = require("Module:parameters").process(frame:getParent().args, params)
	
	local terms = args[1]
	local lang_code = nil
	local id = nil

	if terms and terms[1] and terms[1]:find(":") then
		local first_part = terms[1]
		local colon_pos = first_part:find(":")
		if colon_pos then
			local prefix = first_part:sub(1, colon_pos - 1)
			if prefix:match("^%w[%w%-]*$") then
				lang_code = prefix
			end
		end
	end

	if not terms then
		local title_text = mw.title.getCurrentTitle().text
		local parts = mw.text.split(title_text, "/")
		if not lang_code then lang_code = parts[2] end
		if parts[3] then
			terms = { parts[3] }
		end
		if not id then id = parts[4] end
	end

	if not lang_code then
		local t_str = terms and table.concat(terms, ", ") or ""
		return "Error: Language code could not be determined from term '" .. t_str .. "'."
	end

	local is_multi_term = #terms > 1

	local locs = {}
	local n = {}

	local used_langs = {}
	local map_configs = {}
	local map_groups = {}

	local function load_map_config(code)
		if not used_langs[code] then
			local success, map_data = pcall(require, "Mô đun:dialect map/data/" .. code)
			if success and map_data then
				used_langs[code] = true
				table.insert(map_configs, map_data.config)
				if map_data.groups then
					for _, g in ipairs(map_data.groups) do
						table.insert(map_groups, g)
					end
				end
			end
		end
	end

	load_map_config(lang_code)

	local map_config = map_configs[1]
	local lang = m_lang.getByCode(map_config.lang_code or lang_code)

	local synonym_properties = nil
	local data_module = require("Mô đun:dialect synonyms")

	local function parse_map_term(raw_term, default_code)
		local t = mw.text.trim(raw_term)
		local code = default_code
		local id = nil

		local colon_pos = t:find(":")
		if colon_pos then
			local prefix = t:sub(1, colon_pos - 1)
			if prefix:match("^%w[%w%-]*$") then
				code = prefix
				t = t:sub(colon_pos + 1)
			end
		end

		local id_match = t:match("<id:([^>]+)>")
		if id_match then
			id = id_match
			t = t:gsub("<id:[^>]+>", "")
		end

		return code, t, id
	end

	for _, raw_term in ipairs(terms) do
		local current_full_term = mw.text.trim(raw_term)
		local target_lang, term_text, current_id = parse_map_term(raw_term, lang_code)
		if is_multi_term and not current_id then
			current_id = nil
		elseif not is_multi_term and not current_id then
			current_id = id
		end

		load_map_config(target_lang)

		local dataset, properties, validation
		if term_text == ".all" then
			local path = "Mô đun:dialect synonyms/" .. target_lang
			if mw.title.new(path).exists then
				local lang_data = require(path)
				dataset = { varieties = lang_data.varieties, properties = { title = "Tất cả địa danh" } }
			end
		else
			dataset, _, _ = data_module.get_data(target_lang, term_text, current_id, nil, true)
		end

		if dataset then
			synonym_properties = dataset.properties

			local varieties = dataset.varieties

			local function __traverse(nodes, group_path)
				for _, node in ipairs(nodes) do
					if node[1] then
						local child_path = group_path
						if node.name then
							child_path = group_path .. (group_path ~= "" and "/" or "") .. node.name
						end
						__traverse(node, child_path)
					elseif (term_text == ".all" and node.name) or (node.syns and node.syns[1]) then
						n[node.name] = (n[node.name] or 0)
						local i = 0

						local current_path = group_path
						if node.name then
							current_path = group_path .. (group_path ~= "" and "/" or "") .. node.name
						end

						if term_text == ".all" then
							n[node.name] = n[node.name] + 1
							local point_index = n[node.name]

							local key_form = node.name
							locs[key_form] = locs[key_form] or {}
							local parsed = { term = node.name }
							table.insert(locs[key_form], { node, current_path, point_index, parsed, target_lang })
						else
							for _, parsed in ipairs(node.syns) do
								if type(parsed) == "table" and (parsed.term or parsed.ipa) then
									local key_form = parsed.group or parsed.alt or parsed.term or parsed.ipa
									if is_multi_term then
										key_form = current_full_term .. "::" .. key_form
									end

									if parsed.tr and not parsed.group then
										key_form = key_form .. "::tr:" .. parsed.tr
									end

									n[node.name] = n[node.name] + 1
									local point_index = n[node.name]
	
									locs[key_form] = locs[key_form] or {}
									table.insert(locs[key_form], { node, current_path, point_index, parsed, target_lang })
								end
							end
						end
					end
				end
			end

			__traverse(varieties, "")
		end
	end

	if #map_configs > 1 then
		local min_lat, max_lat = 90, -90
		local min_long, max_long = 180, -180
		local sum_lat, sum_long, sum_zoom = 0, 0, 0

		for _, cfg in ipairs(map_configs) do
			local c_lat = cfg.lat or 0
			local c_long = cfg.long or 0
			sum_lat = sum_lat + c_lat
			sum_long = sum_long + c_long
			sum_zoom = sum_zoom + (cfg.zoom or 6)

			if c_lat < min_lat then min_lat = c_lat end
			if c_lat > max_lat then max_lat = c_lat end
			if c_long < min_long then min_long = c_long end
			if c_long > max_long then max_long = c_long end
		end

		map_config = mw.clone(map_config)
		map_config.lat = sum_lat / #map_configs
		map_config.long = sum_long / #map_configs

		local avg_zoom = sum_zoom / #map_configs
		local lat_spread = max_lat - min_lat
		local long_spread = max_long - min_long
		local max_spread = math.max(lat_spread, long_spread)

		local margin = 360 / (2 ^ avg_zoom)
		local target_span = max_spread + margin

		if target_span > 0 then
			map_config.zoom = math.floor(math.log(720 / target_span) / math.log(2))
		else
			map_config.zoom = math.floor(avg_zoom)
		end
	end

	map_config.legend_format = map_config.legend_format or "grid"
	if map_config.legend_format ~= "grid" and map_config.legend_format ~= "table" then
		return "Error: Invalid map legend format '" .. tostring(map_config.legend_format) .. "'. Supported formats are 'grid' (default) and 'table'."
	end

	if not next(locs) then
		return "Error: Could not load dialect data for any provided terms."
	end

	local forms = {}
	for form, _ in pairs(locs) do
		table.insert(forms, form)
	end

	local function get_handler(code)
		if not code then return nil end
		
		local success, map_handler = pcall(require, "Mô đun:dialect map/handlers/" .. code)
		if success and map_handler then
			return map_handler
		end
	end

	local function make_cache_key(parsed)
		local keys = {}
		for k in pairs(parsed) do
			table.insert(keys, k)
		end
		table.sort(keys)
		
		local parts = {}
		for _, k in ipairs(keys) do
			local v = parsed[k]
			if type(v) == "boolean" then
			elseif type(v) == "table" then
				table.insert(parts, k .. ":" .. table.concat(v, "|"))
			else
				table.insert(parts, k .. ":" .. tostring(v or ""))
			end
		end
		return table.concat(parts, "::")
	end

	local link_cache = {}
	local function __get_formatted_term(lect, parsed)
		local key = make_cache_key(parsed)

		if link_cache[key] then
			return link_cache[key]
		end

		local link_data = {}
		for k, v in pairs(parsed) do
			link_data[k] = v
		end

		if not link_data.lang then
			link_data.lang = lang
		end

		if link_data.q and not link_data.qq then
			link_data.qq = link_data.q
			link_data.q = nil
		elseif link_data.q and link_data.qq then
			if type(link_data.q) ~= "table" then link_data.q = {link_data.q} end
			if type(link_data.qq) ~= "table" then link_data.qq = {link_data.qq} end
			for _, v in ipairs(link_data.q) do
				table.insert(link_data.qq, v)
			end
			link_data.q = nil
		end

		local word
		local handler = get_handler(lect.code_main)
		if handler and handler.format_term then
			word = handler.format_term(lect, link_data)
		elseif handler and handler.make_link then
			word = handler.make_link(link_data)
		else
			if link_data.qq then
				link_data.show_qualifiers = true
			end
			word = m_links.full_link(link_data)
		end

		link_cache[key] = word
		return word
	end

	local function format_term_legend(data_variety, data)
		if type(data) == "string" then return data end

		local term = data.term or ""
		local alt = data.alt or term

		local display_term = (alt and alt ~= term) and alt or term

		local temp_data = {
			term = display_term,
			alt = alt ~= term and alt or nil,
			ipa = data.ipa or nil,
			tr = data.tr or nil,
			ts = data.ts or nil,
			lang = data.lang or data_variety.lang or nil,
			show_qualifiers = false,
		}

		local word
		local handler = get_handler(data_variety.code_main)
		if handler and handler.format_term_legend then
			word = handler.format_term_legend(data_variety, temp_data)
		elseif handler and handler.format_term then
			word = handler.format_term(data_variety, temp_data)
		elseif handler and handler.make_link then
			word = handler.make_link(temp_data)
		else
			word = m_links.full_link(temp_data)
		end

		return word
	end

	local function __get_legend_formatted_term(lect, parsed)
		local key = "legendterm::" .. make_cache_key(parsed)

		if link_cache[key] then
			return link_cache[key]
		end

		local link_data = {}
		for k, v in pairs(parsed) do
			link_data[k] = v
		end

		if not link_data.lang then
			link_data.lang = lang
		end

		local word = format_term_legend(lect, link_data)
		link_cache[key] = word
		return word
	end

	local handler = get_handler(lang_code)

	table.sort(forms, function(a, b)
		return #locs[a] > #locs[b] or (#locs[a] == #locs[b] and a < b)
	end)

	local function __get_coordinates(lect)
		if lect.lat and lect.long then
			return lect.lat, lect.long
		end

		if mw.wikibase and lect.wikidata then
			local qid = lect.wikidata
			if type(qid) == "number" then qid = "Q" .. qid end
			
			local entity = mw.wikibase.getEntity(qid)
			if entity and entity.claims and entity.claims.P625 and entity.claims.P625[1] then
				local snak = entity.claims.P625[1].mainsnak
				if snak and snak.datavalue and snak.datavalue.value then
					return snak.datavalue.value.latitude, snak.datavalue.value.longitude
				end
			end
		end

		return nil, nil
	end

	for idx, form_key in ipairs(forms) do
		local group_id = "term-group-" .. idx

		for _, lect_i in ipairs(locs[form_key]) do
			local lect, group_path, i, parsed, dataset_lang = lect_i[1], lect_i[2], lect_i[3], lect_i[4], lect_i[5]

			local term_display
			if handler and handler.make_map_term_display then
				term_display = handler.make_map_term_display(parsed.term or parsed.ipa, parsed)
			else
				term_display = parsed.alt or parsed.term or parsed.ipa
			end

			local term_lang = parsed.lang or lang
			local marker_code = term_lang:getCode()

			local dataset_lang_obj = m_lang.getByCode(dataset_lang)
			local full_group_path = dataset_lang_obj and dataset_lang_obj:getCanonicalName() or dataset_lang
			if group_path and group_path ~= "" then
				full_group_path = full_group_path .. "/" .. group_path
			end

			local term_url = nil
			if handler and handler.make_map_term_url then
				term_url = handler.make_map_term_url(parsed.term or parsed.ipa, term_lang, parsed)
			end

			local location_name = lect.text_display or lect.name
			local loc_label = location_name
			if handler and handler.format_map_location_name then
				loc_label = handler.format_map_location_name(lect, parsed) or loc_label
			end

			local lat, long = __get_coordinates(lect)

			if lat and long then
				local term_wikitext = __get_formatted_term(lect, parsed)
				local legend_wikitext = __get_legend_formatted_term(lect, parsed)

				if handler and handler.make_map_point then
					table.insert(geojson, handler.make_map_point(lat, long, color,
						term_display, term_url, loc_label, i, n[lect.name], full_group_path, marker_code, group_id, parsed.group, term_wikitext, legend_wikitext))
				else
					table.insert(geojson, export.make_point(lat, long, color,
						term_display, term_url, loc_label, i, n[lect.name], full_group_path, marker_code, group_id, parsed.group, term_wikitext, legend_wikitext))
				end
			end
		end
	end

	local title = (terms[1] or ""):gsub("[0-9%-]", "")
	local term_link
	local is_mixed_langs = false
	local lang_count = 0
	for _ in pairs(used_langs) do lang_count = lang_count + 1 end
	if lang_count > 1 then is_mixed_langs = true end

	if is_multi_term then
		local links = {}
		local mixed_links = {}

		for _, raw_term in ipairs(terms) do
			local target_lang_code, t_term, _ = parse_map_term(raw_term, lang_code)
			local t_lang = m_lang.getByCode(target_lang_code) or lang

			local link = m_links.full_link({
				lang = t_lang,
				term = t_term,
				never_call_transliteration_module = true,
				no_generate_forms = true,
			})
			table.insert(links, link)
			table.insert(mixed_links, t_lang:getCanonicalName() .. " " .. link)
		end

		if is_mixed_langs then
			term_link = table.concat(mixed_links, ", ")
		else
			term_link = table.concat(links, ", ")
		end
	else
		term_link = m_links.full_link(
			{
				lang = lang,
				term = title,
				gloss = synonym_properties.gloss or synonym_properties.meaning,
				never_call_transliteration_module = true,
				no_generate_forms = true,
			}
		)
	end

	local map_header

	if is_mixed_langs then
		 local mixed_list_text = mw.text.listToText(mw.text.split(term_link, ", "), ", ", " và ")
		 local header_text = "Bản đồ của " .. mixed_list_text
		 map_header = export.map_header(header_text)
	elseif handler and handler.make_map_header then
		map_header = handler.make_map_header(lang, title, synonym_properties.gloss or synonym_properties.meaning, term_link)
	else
		local header_text
		if terms[1] == ".all" then
			header_text = "Tất cả địa danh"
		elseif handler and handler.get_map_title then
			header_text = handler.get_map_title(lang, term_link)
		else
			header_text = "Bản đồ biến thể và ngôn ngữ của " .. term_link .. " trong " .. lang:getCanonicalName()
		end
		map_header = export.map_header(header_text)
	end

	if map_groups then
		for _, fam in ipairs(map_groups) do
			table.insert(geojson, export.make_polygon(fam.name, fam.color, fam.points))
		end
	end

	local function compress_data(geo_data)
		local dictionary = {}
		local reverse_dict = {}
		local next_id = 0

		local function get_id(str)
			if not str then return nil end
			if reverse_dict[str] then
				return reverse_dict[str]
			end
			local id = "@@" .. next_id .. "@@"
			dictionary[tostring(next_id)] = str
			reverse_dict[str] = id
			next_id = next_id + 1
			return id
		end

		local compressed_features = {}

		for _, feature in ipairs(geo_data) do
			if feature.geometry and feature.geometry.type == "Point" and feature.properties then
				local p = feature.properties
				local g = feature.geometry
				local coords = g.coordinates

				local lat = math.floor(coords[2] * 10000 + 0.5) / 10000
				local lon = math.floor(coords[1] * 10000 + 0.5) / 10000

				local row = {
					0,
					lat,
					lon,
					get_id(p["wikitext"]) or "",
					get_id(p["legendWikitext"]) or "",
					get_id(p["term"]) or "",
					get_id(p["url"]) or "",
					get_id(p["term-group"]) or "",
					get_id(p["marker-group"]) or "",
					p["marker-color"] or "",
					p["marker-lang"] or "",
					p["group-id"] or "",
					get_id(p["loc"]) or ""
				}
				table.insert(compressed_features, row)
			else
				local new_feat = { ["t"] = "f" }

				if feature.properties then
					local p = feature.properties
					if p.wikitext then p.wikitext = get_id(p.wikitext) end
					if p.legendWikitext then p.legendWikitext = get_id(p.legendWikitext) end
					if p.term then p.term = get_id(p.term) end
					if p.url then p.url = get_id(p.url) end
					if p["term-group"] then p["term-group"] = get_id(p["term-group"]) end
					if p["marker-group"] then p["marker-group"] = get_id(p["marker-group"]) end
					if p["loc"] then p["loc"] = get_id(p["loc"]) end
					new_feat["p"] = p
				end

				if feature.geometry then
					local g = feature.geometry
					local new_g = {}
					if g.type == "Polygon" then new_g["t"] = "pl" else new_g["t"] = g.type end
					new_g["c"] = g.coordinates
					new_feat["g"] = new_g
				end
				table.insert(compressed_features, new_feat)
			end
		end

		return {
			d = dictionary,
			f = compressed_features
		}
	end

	local compressed_data = compress_data(geojson)
	local map_json = mw.text.jsonEncode(compressed_data)
	map_json = mw.text.nowiki(map_json)

	local map = tostring(
		mw.html.create("div")
			:addClass("thumb")
			:addClass("dialect-map__container")
			:addClass("dialect-map-app")
			:attr("data-mapstyle", map_config.mapstyle or "osm-intl")
			:attr("data-mode", terms[1] == ".all" and "all" or "term")
			:attr("data-lat", tostring(map_config.lat))
			:attr("data-lon", tostring(map_config.long))
			:attr("data-zoom", tostring(map_config.zoom))
			:attr("data-show-group", map_config.show_group and "true" or "false")
			:css("position", "relative")
			:css("width", (map_config.width or "100%"))
			:tag("div")
				:addClass("dialect-map-placeholder")
				:css("width", "100%")
				:css("height", (map_config.height or 600) .. "px")
				:css("background-color", "#f8f9fa")
				:css("border", "1px solid #c8ccd1")
				:css("display", "flex")
				:css("align-items", "center")
				:css("justify-content", "center")
				:css("color", "#72777d")
				:wikitext("Loading map...")
				:done()
			:tag("div")
				:addClass("dialect-map-data")
				:css("display", "none")
				:wikitext(map_json)
				:done()
			:done()

	)

	local category = ""
	local current_title = mw.title.getCurrentTitle().prefixedText
	if current_title:find("^Bản mẫu:dialect map/") then
		local categories = {}
		for code, _ in pairs(used_langs) do
			local cat_lang = m_lang.getByCode(code)
			if cat_lang then
				local sort_key = cat_lang:makeSortKey(terms[1] or "")
				table.insert(categories, "[[Thể loại:Bản đồ phương ngữ tương đương " .. cat_lang:getCanonicalName() .. "|" .. sort_key .. "]]")
			end
		end
		category = table.concat(categories, "")
	end

	return map_header .. map .. category
end

return export