Module:fi-dialects/template/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 '' .. text .. " " 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 ' ' .. text .. " " .. suffix .. " " 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 "" .. 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("" .. 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(' ') 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 ' (edit data) ' .. " ''" .. m_common.disclaimer .. "'' " ..			' ' ..			m_common.format_sources(source, true) .. '\n\n' .. '== ' .. heading .. " ==\n" .. (note and note .. "\n" or "") .. [[

List
]] .. parish_table .. [[

]] .. "\n" .. (is_feature and "" or ("")) 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(' ') :done :done)) end map = m_map.show{frame = frame, parishes = parishes, peg = color_peg, size = map_size} end return ' (edit data) ' .. " ''" .. m_common.disclaimer .. "'' \n\n" .. ' Each spot on the map is a parish, with some minor exceptions; see Appendix:Finnish dialects. ' .. xref .. ' \n\n' .. ' Sources: Data of parishes and their areas is based on data from Suomen murteiden sanakirja © Kotimaisten kielten keskus, under the CC BY 4.0 license. Location data is partially extracted from OpenStreetMap © OpenStreetMap contributors, under the Open Database license. See the information for the for its sources and licensing. \n' .. '== Map of Finnish dialects ==\n' .. [[

Dialect list
]] .. require("Module:fi-dialects/list").embed_dialect_list("===") .. "\n" 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 " No data found. (Add some.) " .. require("Module:utilities").format_categories("fi-dial-map missing data") end return synonym_map(frame, title, word_id, data, is_feature) end

return export