Module:Jct/sandbox

From the AARoads Wiki: Read about the road before you go
Jump to navigation Jump to search
local p = {}
-- Change to "" upon deployment.
local moduleSuffix = ""

local parserModuleName = "Module:Road data/parser" .. moduleSuffix
local statenameModuleName = "Module:Jct/statename" .. moduleSuffix -- TODO transition
local cityModuleName = "Module:Jct/city" .. moduleSuffix

local concat = table.concat
local insert = table.insert
local format = mw.ustring.format
local trim = mw.text.trim

local parserModule = require(parserModuleName)
local parser = parserModule.parser
local util = require("Module:Road data/util")


-- Shields
local defaultShieldSize = 24

local function addContextBanner(route, name, suffix, bannerSpec)
	local bannerModule = 'Module:Road data/banners/' .. string.upper(route.country)
	local shield = parser(route, name .. 'shield') or parser(route, 'shield', name, bannerModule)
	
	if shield and shield ~= '' then
		suffix = suffix or parser(route, 'shield', 'suffix', bannerModule)
		if suffix and suffix ~= '' then
			shield = shield .. " " .. suffix
		end
		insert(bannerSpec, {shield .. ".svg", defaultShieldSize})
	end
end

local function bannerSpec(banner, bannerSize, bannerSuffix, route)
	local banners = {}

	if type(banner) == "table" then
		for i, filename in ipairs(banner) do
			local size = type(bannerSize) == "table" and bannerSize[i] or bannerSize or defaultShieldSize
			insert(banners, {filename, size})
		end
	elseif banner ~= '' then
		insert(banners, {banner, bannerSize})
	end

	for _, context in ipairs({'dir', 'to'}) do
		if route[context] then
			addContextBanner(route, context, bannerSuffix, banners)
		end
	end

	return banners
end


local function shieldSpec(route, mainShield, shieldList)
	local shieldSpec = {}
	local shield = parser(route, route.to and 'shieldto' or 'shield') or ''

	if shield == '' then return shieldSpec end
	local shieldsize = parser(route, 'orientation') == "upright" and defaultShieldSize or "x" .. defaultShieldSize

	local banner = parser(route, 'banner') or {}
	local bannersize = defaultShieldSize
	local bannersuffix = parser(route, 'bannersuffix')

	if type(shield) == "table" then
		for i, filename in ipairs(shield) do
			insert(shieldSpec, {
				shield = {filename, shieldsize},
				banners = bannerSpec(banner[i] or banner.all or {}, bannersize[i] or bannersize.all or defaultShieldSize, bannersuffix and bannersuffix[i], route)
			})
		end
	else
		insert(shieldSpec, {
			shield = {shield, shieldsize},
			banners = bannerSpec(banner, bannersize, bannersuffix, route)
		})
	end

	return shieldSpec
end

local missingShields

local shieldExistsCache = {}

local function render(shieldEntry, scale, showLink)
	local shield = shieldEntry.shield
	local banners = shieldEntry.banners

	-- Determine the size of the shield image
	local size
	if shield[2] then
		local width, height = mw.ustring.match(shield[2], "(%d*)x?(%d*)")
		width = tonumber(width)
		height = tonumber(height)
		local sizeparts = {}
		if width then
			table.insert(sizeparts, format("%d", width * scale))
		end
		if height then
			table.insert(sizeparts, format("x%d", height * scale))
		end
		size = table.concat(sizeparts)
	else
		size = format("%s%d", landscape and "x" or "", defaultShieldSize * scale)
	end

	-- Create the HTML code for the shield image
	local shieldCode = format("[[File:%s|%spx|link=|alt=]]", shield[1], size)

	-- Add banners if they exist
	if banners[1] then
		for _, banner in ipairs(banners) do
			shieldCode = format("[[File:%s|%spx|link=|alt=]]<br>%s", banner[1], defaultShieldSize, shieldCode)
		end
	end

	-- Return the HTML wrapped in a <span> for vertical alignment
	return '<span style="display: inline-block; vertical-align: baseline; line-height: 0; text-align: center;">' .. shieldCode .. '</span>'
end

function p.shield(route, scale, showLink, mainShield, shieldList)
    -- Initialize missing shields table
    missingShields = {}

    -- Set default scale if not provided
    scale = scale or 1

    -- Prepare to collect rendered shield HTML
    local rendered = {}

    -- Generate and render each shield specification
    for _, entry in ipairs(shieldSpec(route, mainShield, shieldList)) do
        table.insert(rendered, render(entry, scale, showLink))
    end

    -- Return concatenated shield HTML and missing shields
    return table.concat(rendered), missingShields
end

function p.link(route)
    local nolink = route.nolink
    local abbr = parser(route, 'abbr')

    if nolink then
        return abbr
    else
        local link = parser(route, 'link')
        if not link or link == '' then
            return abbr
        else
            return string.format("[[%s|%s]]", link, abbr)
        end
    end
end

-- Links/abbreviations
local function routeText(route, jctname, frame)
    local link
    local type = route.type

    -- Determine the route link
    if not type or type == '' then
        link = route.route
    else
        link = p.link(route)
    end

    -- Add direction if it exists
    local dir = route.dir and ' ' .. string.lower(route.dir) or ''
    local routeText = link .. dir

    -- Handle route name and formatting
    local name = route.name
    if name and name ~= '' then
        local mainText = jctname and name or routeText
        local parenText = jctname and routeText or name
        return string.format('%s (%s)', mainText, parenText)
    else
        return routeText
    end
end

local function extra(args)
    -- Load icon data
    local extraTypes = mw.loadData('Module:Road data/extra')
    local extraIcon = extraTypes[string.lower(args.extra or '')]
    
    -- Return empty string if no icon data found
    if not extraIcon then return '' end

    -- Set default size for icons
    local size = defaultShieldSize .. 'px'

    -- Determine the icon based on country and state
    local countryIcon = extraIcon[args.country] or extraIcon.default
    if type(countryIcon) == 'table' then
        local localIcon = countryIcon[args.state] or countryIcon.default
        return string.format("[[File:%s|%s|alt=|link=]]", localIcon, size)
    else
        return string.format("[[File:%s|%s|alt=|link=]]", countryIcon, size)
    end
end

local function parseArgs(args)
    -- Normalize state and country
    local state = args.state or args.province or ''
    args.state = state
    local country = args.country and string.upper(args.country) or
                    (mw.loadData("Module:Road data/countrymask")[state] or 'UNK')
    args.country = country

    local params = {'denom', 'county', 'township', 'dab', 'nolink', 'noshield', 'to', 'dir', 'name'}
    local routes = {}
    local routeCount = 1
    local seenTo = false

    while true do
        local routeType = args[routeCount * 2 - 1]
        if not routeType then break end

        local route = {type = routeType, route = args[routeCount * 2]}
        for _, param in pairs(params) do
            route[param] = args[param .. routeCount]
        end

        if args.nolink then route.nolink = args.nolink end
        route.country = country
        route.state = state

        if seenTo then
            if route.to then route.toerror = true end
            route.to = ''
        elseif route.to then
            route.to = true
            seenTo = true
        end

        table.insert(routes, route)
        routeCount = routeCount + 1
    end

    return routes
end


local function prefix(to, num)
    if to and to ~= '' then
        return num == 1 and 'To ' or ' to '
    end
    return num == 1 and '' or '&thinsp;/&thinsp;'
end


local function addErrorMsg(catCode, msg, errorMsg)
    errorMsg.code = errorMsg.code or catCode
    table.insert(errorMsg, string.format('<span style="display: none;">Module:Jct %s</span>', msg))
end


function p._jct(args, frame)
    -- Parse the arguments into route objects
    local routes = parseArgs(args)
    local shields, links, allMissingShields = {}, {}, {}
    local typeErr, toErr = false, false
    frame = frame or mw.getCurrentFrame()
    
    -- Process each route
    for num, route in ipairs(routes) do
        -- Process shields
        if not (args.noshield or route.noshield) then
            local shield, missingShields = p.shield(route)
            table.insert(shields, shield)
            if missingShields[1] then
                table.insert(allMissingShields, table.concat(missingShields, ' / '))
            end
        end
        
        -- Process links with appropriate prefix
        local prefix = prefix(route.to, num)
        if prefix ~= '' then table.insert(links, prefix) end
        table.insert(links, routeText(route, args.jctname, frame))
        
        -- Track errors
        typeErr = typeErr or route.typeerror or false
        toErr = toErr or route.toerror or false
    end
    
    -- Generate output components
    local graphics = table.concat(shields) .. extra(args) .. ' '
    local linkText = table.concat(links)
    local cities = ''
    
    if args.city1 or args.location1 then
        local citiesPrefix = args.citiesprefix and (args.citiesprefix ~= '' and string.format(" %s ", args.citiesprefix) or '') or '&nbsp;'
        local cityModule = require(cityModuleName)
        cities = citiesPrefix .. cityModule.city(args)
    end

    -- Compile error messages
    local errorMsg = {}
    if typeErr then addErrorMsg("§", 'error: Invalid route type', errorMsg) end
    if #allMissingShields > 0 then addErrorMsg("¶", 'error: Missing route marker graphics: ' .. table.concat(allMissingShields, ' / '), errorMsg) end
    if toErr then addErrorMsg("&", 'error: Invalid "to" argument', errorMsg) end
    if args.road then addErrorMsg("∆", 'warning: "road" parameter is deprecated', errorMsg) end
    if args.rdt then addErrorMsg("£", 'warning: "rdt" parameter is deprecated', errorMsg) end
    
    if #errorMsg > 0 then
        local page = mw.title.getCurrentTitle().prefixedText
        table.insert(errorMsg, string.format('[[Category:Jct template errors|%s %s]]', errorMsg.code, page))
        errorMsg = table.concat(errorMsg)
    else
        errorMsg = ''
    end

    -- Return final output
    return graphics .. linkText .. cities .. errorMsg
end


function p.jct(frame)
	-- Import module function to work with passed arguments
	local getArgs = require('Module:Arguments').getArgs
	local args = getArgs(frame, {removeBlanks = false})
	return p._jct(args, frame)
end

function p._roadlink(args, frame)
	local routes = parseArgs(args)
	local links = {}
	local typeErr = false
	local toErr = false
	frame = frame or mw.getCurrentFrame()
	for num,route in ipairs(routes) do
		local prefix = prefix(route.to, num)
		if prefix ~= '' then insert(links, prefix) end
		insert(links, routeText(route, args.jctname, frame))
		typeErr = typeErr or route.typeerror or false
		toErr = toErr or route.toerror or false
	end
	local linkText = concat(links)
	local cities = ''
	if args.city1 or args.location1 then
		local citiesPrefix
		if args.citiesprefix then
			citiesPrefix = args.citiesprefix ~= '' and format(" %s ", args.citiesprefix) or ''
		else
			citiesPrefix = '&nbsp;'
		end
		local cityModule = require(cityModuleName)
		cities = citiesPrefix .. cityModule.city(args)
	end

	local errorMsg = {}
	-- Errors must be reported by the level of severity, most severe first.
	if typeErr then
		-- Report invalid type errors.
		addErrorMsg("2", 'error: Invalid route type', errorMsg)
	end
	if toErr then
		-- Report invalid to errors.
		addErrorMsg("3", 'error: Invalid "to" argument', errorMsg)
	end
	if args.road then
		-- Report deprecated "road" warning.
		addErrorMsg("W", 'warning: "road" parameter is deprecated', errorMsg)
	end
	if #errorMsg > 0 then
		local page = mw.title.getCurrentTitle().prefixedText -- Get transcluding page's title
		-- Add a category for the first, most severe error.
		insert(errorMsg, format('[[Category:Jct template errors|%s %s]]', errorMsg.code, page))
		errorMsg = concat(errorMsg)
	else
		errorMsg = ''
	end

	return linkText .. cities
end

function p.roadlink(frame)
	-- Import module function to work with passed arguments
	local getArgs = require('Module:Arguments').getArgs
	local args = getArgs(frame, {removeBlanks = true})
	return p._roadlink(args, frame)
end

return p