Module:fi-dialects/template/map

From Wiktionary, the free dictionary
Jump to navigation Jump to search

Implements {{fi-dial-map}}.


local export = {}
local m_dial = require("Module:fi-dialects")
local m_common = require("Module:fi-dialects/template/common")

local USE_MAPFRAME = false -- experimental
local MAP_SIZE = 1200
local m_map, map_size, map_width, map_height

if USE_MAPFRAME then
	m_map = require("Module:fi-dialects/map/mapframe")
	local ASPECT_RATIO = 1.1875
	map_width = tostring(MAP_SIZE)
	map_height = tostring(MAP_SIZE * ASPECT_RATIO)
else
	m_map = require("Module:fi-dialects/map")
	map_size = MAP_SIZE .. "px"
end

local dots = {
	"FF150F", "0D8AFF", "59FF0D", "FF0FF1", "E4FF10", 
	"10C7FF", "8210FF", "FF8E0A", "07FFD1", "380DFF", 
	"874F4E", "4C7183", "62814D", "804C7A", "78814D", 
	"4D8178", "694D86", "826949", "57844F", "574D82",
	"FF4E45", "4ABBFF", "8DFF49", "FF42F9", "DAFF4C", 
	"4AFFEA", "9F48FF", "FFB14C", "68FF4C", "6549FF", 
	"783017", "164F73", "307313", "7E187D", "5D7612", 
	"167364", "411777", "734E16", "227416", "31167A", 
}

-- go through synonyms, assign colors to each term, and compile lists to syns.
local function visit(syns, visited, parish, ...)
	local terms = {...}
	if parish then syns[parish] = terms end
	for _, term_w in ipairs(terms) do
		local term = m_common.extract_text(term_w)
		if not visited[term] then
			local next_index = #visited + 1
			table.insert(visited, term)
			visited[term] = dots[next_index]
		end
	end
end

-- makes a "chip" used for the color legend.
local function make_chip(text, color, qualifier)
	if qualifier then text = text .. " " .. require("Module:qualifier").format_qualifier(qualifier) end
	return '<span class="color" style="white-space:nowrap;margin:4px;padding:4px;border:1px solid #' .. color .. ';border-left:2em solid #' .. color .. ';line-height:2.5em">' .. text .. "</span>"
end

-- same as make_chip, but tag "term".
local function make_chip_tag(text, color, qualifier)
	if qualifier then
		suffix = " " .. require("Module:qualifier").format_qualifier(qualifier)
	else
		suffix = ""
	end
	return '<span class="color" style="white-space:nowrap;margin:4px;padding:4px;border:1px solid #' .. color .. ';border-left:2em solid #' .. color .. ';line-height:2.5em"><span class="Latn" lang="fi">' .. text .. "</span>" .. suffix .. "</span>"
end

-- same as make_chip, but link "term".
local function make_chip_link(target, text, color, qualifier)
	return make_chip(m_common.link(target, text), color, qualifier)
end

local function make_wiki_link(target, alt, html)
	if not target then return html end
	return "[[w:" .. target .. "|" .. tostring(mw.html.create("span"):attr("title", alt):wikitext(html)) .. "]]"
end

local function make_formatted_link(term, alt, html)
	return tostring(mw.html.create("span"):attr("title", alt):wikitext("[[" .. term .. "#Finnish|" .. html .. "]]"))
end

local function colors_to_css(colors)
	if type(colors) == "string" then return colors end
	if #colors == 1 then return colors[1] end

	-- create "pie chart"
	local sector = 360 / #colors
	local result = nil
	local fmt = sector == math.floor(sector) and "%0.0f" or "%0.4f"

	for factor, color in ipairs(colors) do
		result = (result and result .. "," or "") .. color .. " " .. string.format(fmt, sector * (factor - 1)) .. "deg " .. string.format(fmt, sector * factor) .. "deg"
	end

	return "conic-gradient(" .. result .. ")"
end

local function make_map_parish_table(parishes_by_form, chip_by_term, label_order)
	local parish_table = nil
    local get_parish = function (p) return m_dial.getParish(p, true) end
	local visited = {}

	local function visit_for_table(form)
		if not form or visited[form] then return end
		visited[form] = true
		local parishes = parishes_by_form[form]
		if not parishes or #parishes == 0 then return end
		table.sort(parishes, function (a, b)
			local pa = get_parish(a)
			local pb = get_parish(b)
			if pa then pa = pa:getFormattedName() end
			if pb then pb = pb:getFormattedName() end
			return (pa or a) < (pb or b)
		end)

		if parish_table then
			parish_table = parish_table .. '\n|-\n'
		else
			parish_table = '{| class="wikitable"\n'
		end

		local formatted_parishes = {}
		for _, parish in ipairs(parishes) do
			local parish_name, parish_tooltip
			local obj = get_parish(parish)
			if obj then
				parish_name = obj:getFormattedName()
				parish_tooltip = obj:getFormattedName() .. ', ' .. obj:getArea():getFormattedName()
			else
				parish_name = parish
				parish_tooltip = nil
			end
			if parish_tooltip then
				table.insert(formatted_parishes, tostring(mw.html.create('span'):wikitext(parish_name):attr('title', parish_tooltip)))
			else
				table.insert(formatted_parishes, parish_name)
			end
		end
		parish_table = parish_table .. '| ' .. (chip_by_term[form] or form) .. '\n| ' .. table.concat(formatted_parishes, ', ')
	end

	if label_order then
		for _, form in ipairs(label_order) do
			visit_for_table(form)
		end
	end

	for form, parishes in pairs(parishes_by_form) do
		visit_for_table(form)
	end

	if parish_table then
		parish_table = parish_table .. '\n|}'
	end
	return parish_table or "''(no forms)''"
end

local function synonym_map(frame, term, word_id, data, is_feature)
	local synonyms = data[is_feature and "data" or "syns"]
	local source = data.source and (type(data.source) == "table" and data.source or { data.source }) or { }
	
	local parishes = { }
	local syns = { }
	local colors = { }
	
	local has_custom_order = is_feature and data.label_order
	
	if has_custom_order then
		for _, label in ipairs(data.label_order) do
			visit(syns, colors, nil, label)
		end
	end
	
	-- gather parishes and their terms
	for parish, terms in pairs(synonyms) do
		if not m_common.special[parish] then
			local result = m_dial.getParish(parish, true)
			if result then
				table.insert(parishes, result)
			end
	
			-- assign each term a color
			if type(terms) == "string" then
				visit(syns, colors, parish, terms)
			else
				-- loadData breaks unpack, we must make a copy
				local terms_copy = {}
				for i, term in ipairs(terms) do terms_copy[i] = term end
				visit(syns, colors, parish, unpack(terms_copy))
			end
		end
	end
	
	if not has_custom_order then
		-- sort and make chips
		table.sort(colors)
	end
	
	local qualifiers = data.qualifiers or { }
	local chips = { }
	local chip_by_term = { }
	for _, term in ipairs(colors) do
		local chip
		if is_feature or data.semantic then
			chip = make_chip(data.labels[term] or term, colors[term], qualifiers[term])
		elseif data.nolink then
			chip = make_chip_tag(data.labels and data.labels[term] or term, colors[term], qualifiers[term])
		else
			chip = make_chip_link(data.links and data.links[term] or term, data.links and ((data.labels and data.labels[term] or nil) or term), colors[term], qualifiers[term])	
		end
		chip_by_term[term] = chip
		table.insert(chips, chip)
	end
	chips = table.concat(chips, " ")
	
	local map

	local function pin_text(parish)
		local terms = syns[parish:getCode()]
		local term_texts = {}
		if is_feature then
			for i, term in ipairs(terms) do term_texts[i] = data.labels[term] or term end
		else
			for i, term in ipairs(terms) do term_texts[i] = m_common.extract_text(term) end
		end
		return parish:getFormattedName() .. ', ' .. parish:getArea():getFormattedName() .. ':\n' .. table.concat(term_texts, is_feature and "; " or ", ")
	end

	local function pin_color(term)
		return '#' .. (colors[is_feature and term or m_common.extract_text(term)] or "000000")
	end

	if USE_MAPFRAME then
		local function make_pin(parish)
			local area = parish:getArea()
			local group = area:getGroup()
			if not group then return nil end
			local branch = group:getBranch()

			local lat, lon = parish:getCoordinates()
			local terms = syns[parish:getCode()]
			
			if #terms < 1 then return nil end

			local features = {}
			-- TODO: need a better solution
			
			local i = 0
			local f = 2 * math.pi / #terms
			if #terms > 1 then
				a = 0.0001
			else
				a = 0.0
			end
			
			for _, term in ipairs(terms) do
				table.insert(features, {
					type = "Feature",
					properties = {
						title = pin_text(parish, area, group, branch),
						["marker-color"] = pin_color(term),
						["marker-size"] = "small"
					},
					geometry = {
						type = "Point",
						coordinates = { lon + a * math.sin(i * f), lat + a * math.cos(i * f) }
					}
				})
				i = i + 1
			end
			return features		
		end
		map = m_map.show{frame = frame, parishes = parishes, pin = make_pin, width = map_width, height = map_height}

	else
		-- complicated peg
		local function render_dot(parish, top, left)
			local terms = syns[parish:getCode()]
			local alt = pin_text(parish)

			local outer = mw.html.create('div')
						:attr('class', 'dot_outer')
						:css('position', 'absolute')
						:css('top', top)		-- positioning
						:css('left', left)
						:tag('div')
							:css('position', 'relative')
							:css('left', '-4px')		-- center (8px / 2 = 4px)
							:css('top', '-4px')
							:css('width', '8px')
							:css('height', '8px')
							:attr('title', alt)

			local color = pin_color(terms[1])
			local multiple_targets = false
			if #terms > 1 then
				local first_link = data.links and data.links[terms[1]] or terms[1]
				color = { color }
				for i = 2, #terms do
					table.insert(color, pin_color(terms[i]))
					multiple_targets = multiple_targets or (not data.links or (data.links[terms[i]] or terms[i]) ~= first_link)
				end
			end

			local dot = outer:tag('span')
					:css('width', '8px')
					:css('height', '8px')
					:css('border-radius', '50%')
					:css('user-select', 'none')
					:css('display', 'inline-block')
					:css('background', colors_to_css(color))
					:css('border', '0.5px solid rgba(0,0,0,0.25)')
					:wikitext('&nbsp;')
			if is_feature or multiple_targets or data.nolink or data.semantic then return tostring(dot:allDone()) end
			return make_formatted_link(terms[1], alt, tostring(dot:allDone()))
		end

		map = m_map.show{frame = frame, parishes = parishes, peg = render_dot, size = map_size}
	end
		
	local heading
	if is_feature then
		heading = data.title or 'Feature map'
	elseif data.semantic then
		heading = 'Dialectal meanings for ' .. m_common.mention(term, data.gloss, data.usage)
	else
		heading = 'Dialectal synonyms for ' .. m_common.mention(term, data.gloss, data.usage)
	end
	
	local note
	if not is_feature and synonyms.common then
		note = "''" .. 'The most commonly found form in dialects is ' .. m_common.mention(synonyms.common) .. '. The map below might not show all parishes where this form is attested.' .. "''"
	end

	local parishes_by_form = { }
	for parish_name, parish_forms in pairs(syns) do
		for _, parish_form in ipairs(parish_forms) do
			parishes_by_form[parish_form] = (parishes_by_form[parish_form] or {})
			table.insert(parishes_by_form[parish_form], parish_name)
		end
	end
	local parish_table = make_map_parish_table(parishes_by_form, chip_by_term, has_custom_order)
	
	return '<div style="float:right;"><small>([[Module:fi-dialects/data/' .. (is_feature and 'feature' or 'word') .. '/' .. word_id .. '|edit data]])</small></div>' ..
			"<p>''" .. m_common.disclaimer .. "''</p>" ..
			'<div style="float:right;"><small>([[commons:File:Finnish dialect location map.svg|background image]])</small></div>' ..
			m_common.format_sources(source, true) .. '\n\n' ..
			'== ' .. heading .. " ==\n" .. (note and note .. "\n" or "") .. [[
{| style="width:100%;" cellspacing="3" cellpadding="5"
|-
| align="center" |
<div style="display:inline-block;">

<p>]] .. chips .. [[</p>

]] .. map .. [[

</div>
|}

=== List ===
]] .. parish_table .. [[

]] .. "\n" .. (is_feature and "" or ("[[Category:Finnish dialect maps|" .. word_id .. "]]"))
end

local function west_east_map(frame)
	local fallback_colors = { ["west"] = "1192a6", ["east"] = "8a06a1" }

	local branch_labels = { ["west"] = "Western Finnish", ["east"] = "Eastern Finnish" }

	local parish_data = mw.loadData("Module:fi-dialects/data/parish").parishes
	
	local west_chips = {}
	local east_chips = {}
	
	local display_groups = frame.args["groups"]

	local palette, xref

	if display_groups then
		local group_colors = {
			-- west (cold colors)
			["Southwest"] = "0080ff",
			["SouthwestTransitional"] = "00d0ff",
			["Tavastia"] = "1cff68",
			["SouthOstrobothnia"] = "2a00fc", 
			["NorthOstrobothnia"] = "5193fc",
			["Lapland"] = "b3d2ff",
			
			-- east (warm colors)
			["Savonia"] = "ffa200",
			["Southeast"] = "e81f00",
		}

		local group_keys = {
			"Southwest", "SouthwestTransitional", "Tavastia",
			"SouthOstrobothnia", "NorthOstrobothnia", "Lapland",
			"Savonia", "Southeast",
		}

		for _, group_code in ipairs(group_keys) do
			local group = m_dial.getGroup(group_code)
			local chips
    		if group:getBranch() == "east" then
    			chips = east_chips
    		elseif group:getBranch() == "west" then
    			chips = west_chips
    		end
    		if chips then
    			table.insert(chips, make_chip(group:getFormattedName(), group_colors[group_code] or fallback_colors[group:getBranch()]))
    		end
		end

		palette = group_colors
		xref = "For a map showing dialect areas rather than groups, see [[Template:fi-dial-map]]."
	else
		local area_colors = {
			-- west (cold colors)
			["Länsi-Pohja"] = "7682cf",
			["Peräpohjola"] = "b3d2ff",
			["Pohjanmaa/Pohjoinen"] = "448bfc", 
			["Pohjanmaa/Keski"] = "095fe8",
			["Pohjanmaa/Etelä"] = "043eb3",
			["Satakunta/Länsi"] = "6be1f2", 
			["Satakunta/Pohjoinen"] = "09bd87",
			["Satakunta/Etelä"] = "2898a8",
			["Varsinais-Suomi/Etelä"] = "448bfc",
			["Varsinais-Suomi/Itä"] = "0677bf",
			["Varsinais-Suomi/Pohjoinen"] = "40aff5", 
			["Varsinais-Suomi/Ylämaa"] = "07c5e0", 
			["Häme/Pohjoinen"] = "52eb82", 
			["Häme/Etelä"] = "21d133",
			["Häme/Kaakko"] = "089908",
			["Kymenlaakso"] = "9acf29",
			
			-- east (warm colors)
			["Keski-Suomi/Pohjoinen"] = "e6c053",
			["Keski-Suomi/Länsi"] = "d1a62a",
			["Keski-Suomi/Etelä"] = "b88c0f",
			["Kainuu"] = "e0de92",
			["Savo/Pohjoinen"] = "e8913a",
			["Savo/Etelä"] = "c26c15",
			["Karjala/Pohjoinen"] = "fc7b12",
			["Karjala/Keski"] = "f2272d",
			["Karjala/Etelä"] = "b81102",
			["Inkeri"] = "9c3b68",
			["Vermlanti"] = "fcf40d",
		}

		local area_colors_sorted = {}
	
		for k, _ in pairs(area_colors) do
			table.insert(area_colors_sorted, k)
		end
	
		table.sort(area_colors_sorted, function (a_code, b_code) 
			local a = m_dial.getArea(a_code)
			local b = m_dial.getArea(b_code)
	
			local a_key = a:getEnglishName()
			local b_key = b:getEnglishName()
			
			-- make sure larger areas like Tavastia stay together
			if a:getSuperArea() then
				a_key = a:getSuperArea():getEnglishName() .. "/" .. a_key
			end
			if b:getSuperArea() then
				b_key = b:getSuperArea():getEnglishName() .. "/" .. b_key
			end
			
			return a_key < b_key
		end)

		for _, area_code in ipairs(area_colors_sorted) do
			local area = m_dial.getArea(area_code)
			local chips = area:getBranch() == "east" and east_chips or west_chips
			table.insert(chips, make_chip(area:getFormattedName(), area_colors[area_code] or fallback_colors[area:getBranch()]))
		end

		palette = area_colors
		xref = "For a map showing dialect groups rather than areas, see [[Template:fi-dial-map/groups]]."
	end

	west_chips = table.concat(west_chips, " ")
	east_chips = table.concat(east_chips, " ")
	
	local parishes = {}
	for k, v in pairs(parish_data) do
		table.insert(parishes, m_dial.getParish(k))
	end
	
	local map

	local function pin_text(parish, area, group, branch)
		area = area or parish:getArea()
		group = group or area:getGroup()
		branch = branch or group:getBranch()
		return parish:getFormattedName() .. ",\n" .. area:getFormattedName() .. ",\n" .. group:getFormattedName() .. ",\n" .. branch_labels[branch]
	end

	local function pin_color(parish, area, group, branch)
		area = area or parish:getArea()
		group = group or area:getGroup()
		return palette[display_groups and group:getCode() or area:getCode()] or fallback_colors[branch]
	end

	if USE_MAPFRAME then
		local function make_pin(parish)
			local area = parish:getArea()
			local group = area:getGroup()
			if not group then return nil end
			local branch = group:getBranch()

			local lat, lon = parish:getCoordinates()

			return {
				type = "Feature",
				properties = {
					title = pin_text(parish, area, group, branch),
					["marker-color"] = pin_color(parish, area, group, branch),
					["marker-size"] = "small"
				},
				geometry = {
					type = "Point",
					coordinates = { lon, lat }
				}
			}		
		end
		map = m_map.show{frame = frame, parishes = parishes, pin = make_pin, width = map_width, height = map_height}

	else
		local function color_peg(parish, top, left)
			local area = parish:getArea()
			local group = area:getGroup()
			if not group then return nil end

			local branch = group:getBranch()
			local alt = pin_text(parish, area, group, branch)
			local color = pin_color(parish, area, group, branch)

			return make_wiki_link(parish:getWikipediaArticle(true), alt, tostring(mw.html.create('div')
						:attr('class', 'dot_outer')
						:css('position', 'absolute')
						:css('top', top)		-- positioning
						:css('left', left)
						:tag('div')
							:css('position', 'relative')
							:css('left', '-4px')		-- center (8px / 2 = 4px)
							:css('top', '-4px')
							:css('width', '8px')
							:css('height', '8px')
							:attr('title', alt)
							:tag('span')
								:css('width', '8px')
								:css('height', '8px')
								:css('border-radius', '50%')
								:css('user-select', 'none')
								:css('display', 'inline-block')
								:css('border', '0.5px solid rgba(0,0,0,0.25)')
								:css('background', '#' .. color)
								:wikitext('&nbsp;')
								:done()
							:done()))
		end
		map = m_map.show{frame = frame, parishes = parishes, peg = color_peg, size = map_size}
	end
	
	return '<div style="float:right;"><small>([[Module:fi-dialects/data/parish|edit data]])</small></div>' .. 
			"<p>''" .. m_common.disclaimer .. "''</p>\n\n" ..
			'<p>Each spot on the map is a parish, with some minor exceptions; see [[Appendix:Finnish dialects]]. ' .. xref .. '</p>\n\n' ..
			'<small>Sources: Data of parishes and their areas is based on data from [https://kaino.kotus.fi/sms/ Suomen murteiden sanakirja] © Kotimaisten kielten keskus, under the CC BY 4.0 license. Location data is partially extracted from [https://www.openstreetmap.org/ OpenStreetMap] © OpenStreetMap contributors, under the Open Database license. See the information for the [[commons:File:Finnish dialect location map.svg#Summary|background image]] for its sources and licensing.</small>\n' ..
			'== Map of Finnish dialects ==\n' .. [[
{| style="width:100%;" cellspacing="3" cellpadding="5"
|-
| align="center" |
<div style="display:inline-block;">

<p>'''Western Finnish''': ]] .. west_chips .. [[</p>

<p>'''Eastern Finnish''': ]] .. east_chips .. [[</p>

<div style="display:inline-block;">

]] .. map .. [[

</div></div>
|}
__NOTOC__
== Dialect list ==
]] .. require("Module:fi-dialects/list").embed_dialect_list("===") .. "\n[[Category:Finnish dialect maps| ]]"
end

function export.show_map(frame)
	local word_id
	local title_text = mw.title.getCurrentTitle().text
	local is_feature = false
	if mw.title.getCurrentTitle().namespace == 10 and title_text == "fi-dial-map/groups" then
		if not frame.args["groups"] then error("groups=1 required") end
		return west_east_map(frame)
	elseif mw.title.getCurrentTitle().namespace == 10 and mw.ustring.find(title_text, "^fi%-dial%-map/") then
		word_id = mw.ustring.gsub(title_text, "^fi%-dial%-map/", "")
	elseif mw.title.getCurrentTitle().namespace == 10 and title_text == "fi-dial-map" then
		if frame.args["groups"] then error("groups not allowed") end
		return west_east_map(frame)
	else
		error("This template can only be used in subpages of [[Template:fi-dial-map]]")
	end
	
	if mw.ustring.find(word_id, "^feature/") then
		is_feature = true
		word_id = mw.ustring.gsub(word_id, "^feature/", "")
	end
	
	local title = word_id
	if mw.ustring.find(title, "%(") then
		title = mw.ustring.match(title, "^[^(]+")
	end
	local module_name = "Module:fi-dialects/data/" .. (is_feature and "feature" or "word") .. "/" .. word_id
	local data_ok, data = pcall(function() return mw.loadData(module_name) end)
	if not data_ok then
		return "<div><em>No data found. ([[" .. module_name .. "|Add some]].)</em></div>" .. require("Module:utilities").format_categories("fi-dial-map missing data")
	end
	return synonym_map(frame, title, word_id, data, is_feature)
end

return export