模組:线路时刻表
外观
local p = {}
local getArgs = require('Module:Arguments').getArgs
---@param funcName string
---@return fun(frame: frame)
local function makeInvokeFunction(funcName)
return function(frame)
local args = getArgs(frame, { parentOnly = true })
return p[funcName](args, frame)
end
end
---@type fun(_: {[1]: string}): string
local delink = require('Module:delink').delink
---@alias spanTerminus { [1]: string[], [2]: string[] }
---@alias spanDay { [1]: spanTerminus, [2]: spanTerminus }
---@alias span { line: string, type: string?, stations: string[], data: { [string]: spanDay } }
---@alias spanLTerminus { [1]: string[], [2]: string[][] }
---@alias spanLDay spanLTerminus[]
---@alias spanL { line: string, type: string?, stations: string[], data: { [string]: spanLDay }, override_termini: string[], sub_termini: { [1]: string[], [2]: string[], scope: 'last' } }
---@alias periodDay { [1]: string[], [2]: string[] }
---@alias pointDay { [1]: { [1]: integer }, [2]: { [1]: integer } }
---@alias point { line: string|string[], type: string|string[]?, periods: periodDay[], data: { [string]: pointDay[] } }
---@alias timeEntry { days: string[], ref: string, timespans: (span|spanL)[], timepoints: point[] }
---@type timeEntry
local data
---@alias adjType { title: string, color: string, circular: boolean, ['left terminus']: string, ['right terminus']: string }
---@alias adjLine { types: { [string]: adjType }, title: string, color: string, circular: boolean, ['left terminus']: string, ['right terminus']: string }
---@alias adjEntry { lines: { [string]: adjLine }, aliases: { [string]: string } }
---@type adjEntry
local adj_data
---加载时刻数据
---@param system string
---@return timeEntry?
local function getData(system)
local success, result = pcall(mw.loadData, 'Module:线路时刻表/' .. system)
if success then
return result
else
return nil
end
end
---加载线网数据
---@param system string
---@return adjEntry?
local function getAdjData(system)
local success, result = pcall(mw.loadData, 'Module:Adjacent stations/' .. system)
if success then
return result
else
return nil
end
end
---加载数据
---@param system string
local function initData(system)
local d = getData(system)
if d then
data = d
else
error('不存在模块:线路时刻表/' .. system)
end
local a = getAdjData(system)
if a then
adj_data = a
else
error('不存在模块:Adjacent stations/' .. system)
end
end
---获取线路数据
---@param line string
---@return adjLine
local function getLineData(line)
if line then
if adj_data['aliases'] then
line = adj_data['aliases'][mw.ustring.lower(line)] or line
end
local default = adj_data['lines']['_default'] or {}
local line_data = {}
-- Shadow clone to avoid edit immutable objects
for k, v in pairs(adj_data['lines'][line] or {}) do
line_data[k] = v
end
for k, v in pairs(default) do
if v then line_data[k] = line_data[k] or v end
end
line_data['title'] = line_data['title'] and mw.ustring.gsub(line_data['title'], '%%1', line)
return line_data
else
error('Null argument')
end
end
---获取支线数据
---@param line_data adjLine
---@param _type string
---@return adjType
local function getTypeData(line_data, _type)
_type = adj_data.aliases and adj_data.aliases[mw.ustring.lower(_type)] or _type
return line_data.types and line_data.types[_type] or {}
end
---获取线路标题
---@param line string
---@param _type string
---@return string
local function getTitle(line, _type)
local line_data = getLineData(line)
if _type then
local type_data = getTypeData(line_data, _type)
return line_data.title .. (type_data.title or _type)
else
return line_data.title
end
end
---获取支线标题
---@param line string
---@param _type string
---@return string
local function getSubtitle(line, _type)
local line_data = getLineData(line)
if _type then
local type_data = getTypeData(line_data, _type)
return type_data.title or _type
else
error('Null argument')
end
end
---获取线路颜色
---@param line string
---@param _type string
---@return string
local function getColor(line, _type)
local line_data = getLineData(line)
if _type then
local type_data = getTypeData(line_data, _type)
return '#' .. (type_data.color or line_data.color)
else
return '#' .. line_data.color
end
end
---@alias terminus string|{ [integer]: string, via: string? }
---@alias termini { [1]: terminus, [2]: terminus }
---获取线路终点
---@param line string
---@param _type string
---@return termini
local function getTermini(line, _type)
local line_data = getLineData(line)
if _type then
local type_data = getTypeData(line_data, _type)
return {
type_data['right terminus'] or line_data['right terminus'],
type_data['left terminus'] or line_data['left terminus']
}
else
return {
line_data['right terminus'],
line_data['left terminus']
}
end
end
---获取车站链接
---@param frame frame
---@param name string
---@param system string
---@param line string
---@param _type string
---@return string
local function getStation(frame, name, system, line, _type)
return require('Module:Adjacent stations')._station(
{ system, name, line, _type },
frame)
end
---构造终点站表头
---@param frame frame
---@param terminus terminus
---@param system string
---@param line string
---@param _type string
---@return string
local function makeTerminus(frame, terminus, system, line, _type)
local circular
if _type and _type ~= '' then
circular = adj_data.lines[line].types[_type].circular
if circular == nil then
circular = adj_data.lines[line].circular
end
else
circular = adj_data.lines[line].circular
end
if type(terminus) == 'table' then
if circular then
return table.concat(terminus, '或')
else
local names = {}
for _, name in ipairs(terminus) do
table.insert(names, getStation(frame, name, system, line, _type))
end
return '往' .. table.concat(names, '或')
end
else
if circular then
return terminus
else
return '往' .. getStation(frame, terminus, system, line, _type)
end
end
end
---构造次级终点站表头
---@param frame frame
---@param subTerminus string
---@param system string
---@param line string
---@param _type string
---@return string
local function makeSubTerminus(frame, subTerminus, system, line, _type)
return '至' .. getStation(frame, subTerminus, system, line, _type)
end
---检测车站是否运营
---@param t span|spanDay|spanTerminus|spanL|spanLDay|spanLTerminus|string
---@return boolean
local function checkOperated(t)
if type(t) == 'table' then
return checkOperated(t[1])
else
return t ~= nil
end
end
---平面化数组
---@param input table
---@param flattened string[]?
---@return string[]
local function flattenMatrix(input, flattened)
flattened = flattened or {}
for i, element in ipairs(input) do
if type(element) == 'table' then
flattenMatrix(element, flattened)
else
table.insert(flattened, element)
end
end
return flattened
end
---克隆单层数组
---@generic T
---@param input T[]?
---@return T[]?
local function cloneArray(input)
if not input then return nil end
local result = {}
for i, item in ipairs(input) do
result[i] = item
end
return result
end
---构建车站信息框时刻
---@param args string[]
---@param frame frame
---@return html
function p._as_station(args, frame)
local system = args[1]
local station = args[2]
local all_days = args[3] -- 使用完整时刻表
initData(system)
local days
if all_days then
days = cloneArray(data.days)
else
days = { data.days[1] }
end
local tbl = mw.html.create('table')
:addClass('wikitable')
:addClass('station-infobox-timetable')
--表头
if #days > 1 then
local tr_day = mw.html.create('tr')
tr_day
:tag('th')
:attr('colspan', 3):attr('rowspan', 2)
:wikitext(frame:preprocess(data.ref)):done()
for _, day in ipairs(days) do
tr_day
:tag('th')
:attr('colspan', 2):attr('scope', 'colgroup')
:wikitext(day):done()
end
tr_day:allDone()
tbl:node(tr_day)
local tr_type = mw.html.create('tr')
for _ = 1, #days do
tr_type
:tag('th')
:attr('scope', 'col')
:wikitext('首班'):done()
:tag('th')
:attr('scope', 'col')
:wikitext('末班'):done()
end
tr_type:allDone()
tbl:node(tr_type)
elseif #days == 1 then
tbl:tag('tr')
:tag('th')
:attr('colspan', 3)
:wikitext(days[1], frame:preprocess(data.ref)):done()
:tag('th')
:attr('scope', 'col')
:wikitext('首班'):done()
:tag('th')
:attr('scope', 'col')
:wikitext('末班'):done()
:done()
else
error('缺少运行图元信息')
end
for _, route_data in ipairs(data.timespans) do
local _data = route_data.data[station]
if _data and checkOperated(_data) then
-- 允许系统内部分线路退化为单day
if #days > 1 and #_data == 1 then
for _ = 1, #days - #_data do
table.insert(_data, _data[1])
end
end
local color = getColor(route_data.line, route_data.type)
local termini = route_data.override_termini or getTermini(route_data.line, route_data.type)
-- 有子终点站
if route_data.sub_termini then
if route_data.sub_termini.scope == 'last' then
---@cast _data spanLDay
local sum_row = 0
local t_map = {}
local s_map = {}
-- 预处理:有效行
for t, terminus in ipairs(termini) do
table.insert(s_map, {})
for s, _ in ipairs(route_data.sub_termini[t]) do
for d = 1, #days do
if _data[d][2][t][s] and _data[d][2][t][s] ~= '' then
table.insert(s_map[#s_map], s)
break
end
end
end
-- 处理终点站(末班为空)
if #s_map[#s_map] == 0 then
if terminus == station then
table.insert(s_map[#s_map], 0) -- 标记终点站
end
end
if #s_map[#s_map] == 0 then
table.remove(s_map)
else
table.insert(t_map, t)
end
sum_row = sum_row + #s_map[#s_map]
end
-- 线路色条
local tr = mw.html.create('tr')
tr:tag('td')
:addClass('bar')
:attr('rowspan', sum_row):attr('role', 'rowheader')
:attr('aria-label', delink { getTitle(route_data.line, route_data.type) })
:css('background-color', color):done()
for it, t in ipairs(t_map) do
local terminus = termini[t]
if it > 1 then
tr = mw.html.create('tr')
end
-- 终点站
if s_map[it][1] == 0 then
tr:tag('td')
:attr('colspan', 2):addClass('table-rh'):attr('role', 'rowheader')
:wikitext(makeTerminus(frame, terminus, system, route_data.line, route_data.type)):done()
:tag('td')
:addClass('note'):attr('colspan', 2 * #days)
:wikitext('终点站'):done()
:allDone()
tbl:node(tr)
-- 仅全程
elseif s_map[it][1] == 1 and #s_map[it] == 1 and terminus == route_data.sub_termini[t][1] then
tr:tag('td')
:attr('colspan', 2):addClass('table-rh'):attr('role', 'rowheader')
:wikitext(makeTerminus(frame, terminus, system, route_data.line, route_data.type)):done()
for d = 1, #days do
tr:tag('td')
:attr('rowspan', #s_map[it])
:wikitext(_data[d][1][t]):done()
:tag('td')
:wikitext(_data[d][2][t][1]):done()
end
tbl:node(tr:allDone())
elseif s_map[it][1] > 0 then
tr:tag('td')
:attr('rowspan', #s_map[it]):addClass('table-rh-parent'):attr('role', 'rowheader')
:wikitext(makeTerminus(frame, terminus, system, route_data.line, route_data.type)):done()
for is, s in ipairs(s_map[it]) do
local sub_terminus = route_data.sub_termini[t][s]
if is > 1 then
tr = mw.html.create('tr')
end
if terminus == sub_terminus then
tr:tag('td')
:addClass('table-rh-child'):attr('role', 'rowheader')
:wikitext('全程'):done()
else
tr:tag('td')
:addClass('table-rh-child'):attr('role', 'rowheader')
:wikitext(makeSubTerminus(frame, sub_terminus, system, route_data.line,
route_data.type)):done()
end
for d = 1, #days do
if is == 1 then
tr:tag('td')
:attr('rowspan', #s_map[it])
:wikitext(_data[d][1][t]):done()
end
tr:tag('td')
:wikitext(_data[d][2][t][s]):done()
end
tbl:node(tr:allDone())
end
else
error('无效的索引值')
end
end
else
error('不支持的子终点站作用域') -- 目前仅实现末班车的子终点站
end
-- 无子终点站
else
---@cast _data spanDay
local t_map = {}
-- 预处理:有效行
for t, terminus in ipairs(termini) do
if terminus == station then
table.insert(t_map, t)
else
for d = 1, #days do
if (_data[d][1][t] and _data[d][1][t] ~= '') or (_data[d][2][t] and _data[d][2][t] ~= '') then
table.insert(t_map, t)
break
end
end
end
end
local tr = mw.html.create('tr')
tr:tag('td')
:addClass('bar')
:attr('rowspan', #t_map):attr('role', 'rowheader')
:attr('aria-label', delink { getTitle(route_data.line, route_data.type) })
:css('background-color', color):done()
for it, t in ipairs(t_map) do
local terminus = termini[t]
if it > 1 then
tr = mw.html.create('tr')
end
tr:tag('td')
:attr('colspan', 2):addClass('table-rh'):attr('role', 'rowheader')
:wikitext(makeTerminus(frame, terminus, system, route_data.line, route_data.type)):done()
if terminus == station then
tr:tag('td')
:addClass('note'):attr('colspan', 2 * #days)
:wikitext('终点站'):done()
else
for d = 1, #days do
tr:tag('td')
:wikitext(_data[d][1][t]):done()
tr:tag('td')
:wikitext(_data[d][2][t]):done()
end
end
tr:allDone()
tbl:node(tr)
end
end
end
end
return tbl:allDone()
end
---构建线路时刻
---@param args string[]
---@param frame frame
---@return html?
function p._as_line(args, frame)
local system = args[1]
local line = args[2]
local _type = args[3]
initData(system)
for _, route_data in ipairs(data.timespans) do
if line == route_data.line and _type == route_data.type then
local tbl = mw.html.create('table')
:addClass('wikitable')
:addClass('line-timetable')
local color = getColor(route_data.line, route_data.type)
local termini = cloneArray(route_data.override_termini) or getTermini(route_data.line, route_data.type)
local num_cols
local num_ref_rows = 2
local days
if #cloneArray(route_data.data[route_data.stations[1]]) == 1 then -- 允许线路退化为无 day
days = { data.days[1] }
else
days = cloneArray(data.days)
end
if #days > 1 then num_ref_rows = num_ref_rows + 1 end
if route_data.sub_termini then num_ref_rows = num_ref_rows + 1 end
local ref = mw.html.create('th')
:addClass('ref'):attr('rowspan', num_ref_rows)
:wikitext('车站<br/>', frame:preprocess(data.ref))
:allDone()
local bar = mw.html.create('tr')
---@type html[]
local hr = {}
if route_data.sub_termini then
if route_data.sub_termini.scope == 'last' then
local num_f_cols = #termini
local num_l_cols = #flattenMatrix(route_data.sub_termini)
num_cols = #days * (num_f_cols + num_l_cols)
bar:tag('td')
:addClass('bar'):attr('colspan', num_cols + 1):css('background-color', color)
:allDone()
local hT1 = mw.html.create()
local hT2 = mw.html.create()
local hS = mw.html.create()
for t, terminus in ipairs(termini) do
local sub_termini = cloneArray(route_data.sub_termini[t])
local th_text = makeTerminus(frame, terminus, system, line, _type)
hT1:tag('th')
:addClass('major'):attr('rowspan', 2)
:wikitext(th_text)
:done()
hT2:tag('th')
:addClass('major'):attr('colspan', #sub_termini)
:wikitext(th_text)
:done()
for s, sub_terminus in ipairs(sub_termini) do
if terminus == sub_terminus then
hS:tag('th'):addClass('minor'):wikitext('全程'):done()
else
hS:tag('th')
:addClass('minor')
:wikitext(makeSubTerminus(frame, sub_terminus, system, line, _type))
:done()
end
end
end
local hFL = mw.html.create()
:tag('th'):attr('colspan', num_f_cols):wikitext('首班'):done()
:tag('th'):attr('colspan', num_l_cols):wikitext('末班')
:allDone()
hr[1] = mw.html.create('tr')
hr[2] = mw.html.create('tr')
hr[3] = mw.html.create('tr')
hr[4] = mw.html.create('tr')
if #days > 1 then
hr[1]:node(ref)
else
hr[2]:node(ref)
end
for d, day in ipairs(days) do
hr[1]:tag('th'):attr('colspan', num_f_cols + num_l_cols):wikitext(day):done()
hr[2]:node(hFL)
hr[3]:node(hT1):node(hT2)
hr[4]:node(hS)
end
hr[1]:allDone()
hr[2]:allDone()
hr[3]:allDone()
hr[4]:allDone()
else
error('不支持的子终点站作用域')
end
else
num_cols = 2 * #days * #termini
bar:tag('td')
:addClass('bar'):attr('colspan', num_cols + 1):css('background-color', color)
:allDone()
local hT = mw.html.create('')
for t, terminus in ipairs(termini) do
hT:tag('th'):addClass('th'):wikitext(makeTerminus(frame, terminus, system, line, _type)):done()
end
hT:allDone()
local hFL = mw.html.create()
:tag('th'):attr('colspan', #termini):wikitext('首班'):done()
:tag('th'):attr('colspan', #termini):wikitext('末班')
:allDone()
hr[1] = mw.html.create('tr')
hr[2] = mw.html.create('tr')
hr[3] = mw.html.create('tr')
if #days > 1 then
hr[1]:node(ref)
else
hr[2]:node(ref)
end
for _, day in ipairs(days) do
hr[1]:tag('th'):attr('colspan', 2 * #termini):wikitext(day):done()
hr[2]:node(hFL)
hr[3]:node(hT):node(hT)
end
hr[1]:allDone()
hr[2]:allDone()
hr[3]:allDone()
end
if #days == 1 then table.remove(hr, 1) end
tbl:node(bar)
for _, row in ipairs(hr) do
tbl:node(row)
end
tbl:node(bar)
local tr
for _, station in ipairs(route_data.stations) do
local _data = flattenMatrix(route_data.data[station])
tr = mw.html.create('tr'):tag('td'):addClass('table-rh'):wikitext(station):done()
if checkOperated(_data) then
for _, time in ipairs(_data) do
tr:tag('td'):wikitext(time):done()
end
else
tr:tag('td'):addClass('closed'):attr('colspan', num_cols):wikitext('未运营'):done()
end
tbl:node(tr:allDone())
end
tbl:node(bar)
return tbl:allDone()
end
end
end
---@param str string
---@return integer
---@return integer
local function parseTime(str)
return tonumber(string.sub(str, 1, 2)) or 0, tonumber(string.sub(str, 4, 5)) or 0
end
---@param h integer
---@param m integer
---@param other integer
---@return integer
---@return integer
local function addMinutes(h, m, other)
h = h
m = m + other
if m >= 60 then
local x = math.floor(m / 60)
h = h + x
m = m - 60 * x
end
return h, m
end
---构建车站车次时刻
---@param args string[]
---@param frame frame
---@return html
function p._as_station_ext(args, frame)
local system = args[1]
local station = args[2]
initData(system)
local days = cloneArray(data.days)
local tbl = mw.html.create('table')
:addClass('wikitable')
:addClass('station-route-timetable')
for _, route_data in ipairs(data.timepoints) do
local _data = route_data.data[station]
local route_periods = cloneArray(route_data.periods)
if _data then
if type(route_data.line) == 'table' then
if type(route_data.type) == 'table' then
for l, line in ipairs(route_data.line --[[ @as string[] ]]) do
tbl:tag('tr')
:tag('td')
:addClass('bar'):addClass('composed'):attr('colspan', 2 * #days + 1)
:css('background-color', getColor(line, route_data.type[l]))
:allDone()
end
else
for _, line in ipairs(route_data.line --[[ @as string[] ]]) do
tbl:tag('tr')
:tag('td')
:addClass('bar'):addClass('composed'):attr('colspan', 2 * #days + 1)
:css('background-color', getColor(line, route_data.type --[[ @as string ]]))
:allDone()
end
end
else
tbl:tag('tr')
:tag('td')
:addClass('bar'):attr('colspan', 2 * #days + 1)
:css('background-color',
getColor(route_data.line --[[ @as string ]], route_data.type --[[ @as string ]]))
:allDone()
end
local num_days = #route_periods
local day_span = 1
if num_days < #days then
assert(num_days == 1, #route_periods)
day_span = #days
end
---@type { [1]: { [integer]: string[] }, [2]: { [integer]: string[] } }[]
local points = {}
local h_min = 23
local h_max = 0
local update_h_bound = function(h)
h_min = math.min(h, h_min)
h_max = math.max(h, h_max)
end
for d = 1, num_days, 1 do
local _points = {}
_points[1] = {}
for _, point in ipairs(route_periods[d][1]) do
local h, m = parseTime(point)
h, m = addMinutes(h, m, _data[d][1][1])
h = h % 24
if _points[1][h] then
table.insert(_points[1][h], string.format('%02d', m))
else
_points[1][h] = { string.format('%02d', m) }
update_h_bound(h)
end
end
_points[2] = {}
for _, point in ipairs(route_periods[d][2]) do
local h, m = parseTime(point)
h, m = addMinutes(h, m, _data[d][2][1])
h = h % 24
if _points[2][h] then
table.insert(_points[2][h], string.format('%02d', m))
else
_points[2][h] = { string.format('%02d', m) }
update_h_bound(h)
end
end
table.insert(points, _points)
end
tbl:tag('tr'):tag('th')
:addClass('ref'):wikitext(frame:preprocess(getSubtitle(
route_data.line[1] or route_data.line,
route_data.type[1] or route_data.type)))
:attr('colspan', 2 * #days + 1)
:done():done()
local termini = getTermini(route_data.line[1] or route_data.line,
route_data.type[1] or route_data.type)
tbl:tag('tr')
:tag('th')
:wikitext(makeTerminus(frame, termini[1], system, route_data.line[1] or route_data.line,
route_data.type[1] or route_data.type))
:attr('colspan', #days)
:done()
:tag('th'):wikitext('时')
:attr('rowspan', num_days > 1 and 2 or 1)
:done()
:tag('th')
:wikitext(makeTerminus(frame, termini[2], system, route_data.line[1] or route_data.line,
route_data.type[1] or route_data.type))
:attr('colspan', #days)
:done()
:done()
if num_days > 1 then
local tr = mw.html.create('tr')
for d = #days, 1, -1 do
tr:tag('th'):wikitext(days[d]):done()
end
for d = 1, #days do
tr:tag('th'):wikitext(days[d]):done()
end
tr:allDone()
tbl:node(tr)
end
for h = h_min, h_max do
local tr = mw.html.create('tr')
for d = num_days, 1, -1 do
tr:tag('td')
:attr('style', 'direction: rtl')
:wikitext(
frame:expandTemplate {
title = 'hlist',
args = points[d][1][h]
})
:attr('colspan', day_span)
:done()
end
tr:tag('td'):addClass('table-rh'):wikitext(string.format('%02d', h)):done()
for d = 1, num_days do
tr:tag('td')
:wikitext(
frame:expandTemplate {
title = 'hlist',
args = points[d][2][h]
})
:attr('colspan', day_span)
:done()
end
tr:allDone()
tbl:node(tr)
end
end
end
return tbl:allDone()
end
p.as_station = makeInvokeFunction('_as_station')
p.as_line = makeInvokeFunction('_as_line')
p.as_station_ext = makeInvokeFunction('_as_station_ext')
return p