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 = terms end
for _, term_w in ipairs(terms) do
local term = term_w
if not visited then
local next_index = #visited + 1
table.insert(visited, term)
visited = dots
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, id, color, qualifier)
return make_chip(m_common.link(target, text, id), color, qualifier)
end
local function make_wiki_link(target, alt, html)
if not target then return html end
return "]"
end
local function make_formatted_link(term, alt, html)
local text, id = m_common.parse_term(term)
local id_suffix = ""
if id then id_suffix = ":_" .. mw.ustring.gsub(id, " ", "_") end
return tostring(mw.html.create("span"):attr("title", alt):wikitext("]"))
end
local function colors_to_css(colors)
if type(colors) == "string" then return colors end
if #colors == 1 then return colors 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 fi_sort(a, b)
return m_common.get_sort_key(a) < m_common.get_sort_key(b)
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 then return end
visited = true
local parishes = parishes_by_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 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
local forms_list = {}
for form, parishes in pairs(parishes_by_form) do
table.insert(forms_list, form)
end
table.sort(forms_list, fi_sort)
for _, form in ipairs(forms_list) 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
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 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 = 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, fi_sort)
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 or term, colors, qualifiers)
else
local text, id = m_common.parse_term(term)
if data.nolink then
chip = make_chip_tag(data.labels and data.labels or text, colors, qualifiers)
else
local target = data.links and data.links or text
local label = data.labels and (data.labels and data.labels or nil) or text
chip = make_chip_link(target, label, id, colors, qualifiers)
end
end
chip_by_term = chip
table.insert(chips, chip)
end
chips = table.concat(chips, " ")
local map
local function pin_text(parish)
local terms = syns
local term_texts = {}
if is_feature then
for i, term in ipairs(terms) do term_texts = data.labels or term end
else
for i, term in ipairs(terms) do term_texts = 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 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
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),
= pin_color(term),
= "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
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)
local multiple_targets = false
if #terms > 1 then
local first_link = data.links and data.links] or terms
color = { color }
for i = 2, #terms do
table.insert(color, pin_color(terms))
multiple_targets = multiple_targets or (not data.links or (data.links] or terms) ~= 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, alt, tostring(dot:allDone()))
end
map = m_map.show{frame = frame, parishes = parishes, peg = render_dot, size = map_size} .. require("Module:TemplateStyles")("Module:fi-dialects/style.css")
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 = (parishes_by_form or {})
table.insert(parishes_by_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>(])</small></div>' ..
"<p>''" .. m_common.disclaimer .. "''</p>" ..
'<div style="float:right;"><small>(])</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 ("]"))
end
local function west_east_map(frame)
local fallback_colors = { = "1192a6", = "8a06a1" }
local branch_labels = { = "Western Finnish", = "Eastern Finnish" }
local parish_data = mw.loadData("Module:fi-dialects/data/parish").parishes
local west_chips = {}
local east_chips = {}
local display_groups = frame.args
local palette, xref
if display_groups then
local group_colors = {
-- west (cold colors)
= "0080ff",
= "00d0ff",
= "1cff68",
= "2a00fc",
= "5193fc",
= "b3d2ff",
-- east (warm colors)
= "ffa200",
= "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 or fallback_colors))
end
end
palette = group_colors
xref = "For a map showing dialect areas rather than groups, see ]."
else
local area_colors = {
-- west (cold colors)
= "7682cf",
= "b3d2ff",
= "448bfc",
= "095fe8",
= "043eb3",
= "6be1f2",
= "09bd87",
= "2898a8",
= "448bfc",
= "0677bf",
= "40aff5",
= "07c5e0",
= "52eb82",
= "21d133",
= "089908",
= "9acf29",
-- east (warm colors)
= "e6c053",
= "d1a62a",
= "b88c0f",
= "e0de92",
= "e8913a",
= "c26c15",
= "fc7b12",
= "f2272d",
= "b81102",
= "9c3b68",
= "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 or fallback_colors))
end
palette = area_colors
xref = "For a map showing dialect groups rather than areas, see ]."
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
end
local function pin_color(parish, area, group, branch)
area = area or parish:getArea()
group = group or area:getGroup()
return palette or fallback_colors
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),
= pin_color(parish, area, group, branch),
= "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 '<div style="float:right;"><small>(])</small></div>' ..
"<p>''" .. m_common.disclaimer .. "''</p>\n\n" ..
'<p>Each spot on the map is a parish, with some minor exceptions; see ]. ' .. xref .. '</p>\n\n' ..
'<small>Sources: Data of parishes and their areas is based on data from © Kotimaisten kielten keskus, under the CC BY 4.0 license. Location data is partially extracted from © OpenStreetMap contributors, under the Open Database license. See the information for the ] 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]"
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 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 then error("groups not allowed") end
return west_east_map(frame)
else
error("This template can only be used in subpages of ]")
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. (].)</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