文档图示 模块文档[查看] [编辑] [历史] [清除缓存]

本模块是实现一系列交通相关模板功能的基础模块,支持管理各城市公共汽车系统的数据资料,目前主要用于生成一个显示公交线路资料的表格。

子模块一览 编辑

目前,各公交系统均使用独立的模板,并依赖本模块子页面的相关资料。下表为本模块已建立的子模块。您也可以仿照后文所述的格式新建子模块(并不限于中国大陆城市),并将其添加至下表。欢迎各位对已有资料进行定期维护更新。

相互引用情况 编辑

珠三角各系统
位置
Module:CNBUS/
被引用系统
肇庆 珠海 江门 中山 佛山 广州 东莞 惠州 深圳
系统 肇庆 不適用 ZQ/foshan
珠海 不適用 ZH/jiangmen ZH/data
ZH/zhongsha
江门 JM/zhuhai 不適用 JM/zhongshan JM/foshan
中山 ZS/zhuhai ZS/data 不適用 ZS/foshan
佛山 FS/zhaoqing FS/data
FS/jiangmen
FS/data
FS/zhongshan
不適用 FS/guangzhou
广州 GZ/foshan 不適用 GZ/dongguan
东莞 DG/guangzhou 不適用 DG/huizhou DG/shenzhen
惠州 HZ/dongguan 不適用 HZ/shenzhen
深圳 SZ/dongguan SZ/huizhou 不適用
潮汕地区各系统
位置
Module:CNBUS/
被引用系统
汕头 潮州 揭阳
系统 汕头 不適用 ST/jieyang
潮州 CZ/shantou 不適用 CZ/jieyang
揭阳 JY/shantou 不適用

接口一览 编辑

直接调用接口的模板的参数表(传入)和其调用接口时显式指定的参数表(传出)都会被识别,两者优先级参见Module:Arguments。由本模块导出的模板可能还提供了其他别名。

接口参数列表
参数 说明 列表 折叠列表 运营商颜色 线路名称 别名与注释
{{{1}}} {{{2}}} 线路代码列表 需要 需要 不適用 单个
{{{city}}} 城市代码 需要 需要 需要 需要 各城市模板默认填写
{{{area}}} 区域代码 需要 需要 不適用 需要 部分模板提供{{{loc}}}别名
{{{operator}}} 运营商代码 不適用 不適用 需要 不適用 {{{company}}}
区别于{{{operators}}}
{{{start}}} 是否开始表格
输出<table>开标签及表头
可选 可选 不適用 不適用 默认为真
{{{end}}} 是否结束表格
输出</table>
可选 可选 不適用 不適用 默认为真
{{{header}}} 表格标题 可选 可选 不適用 不適用 {{{info}}} {{{station}}}
依赖于{{{start}}}
{{{type}}} 列表样式 可选 不適用 不適用 不適用 BRT:覆盖{{{fare}}}{{{operators}}}{{{vehicles}}}
{{{time}}} 是否显示时间 可选 不適用 不適用 不適用
{{{fare}}} 是否显示票价 可选 不適用 不適用 不適用 默认为真
{{{operators}}} 是否显示运营商 可选 不適用 不適用 不適用 默认为真
区别于{{{operator}}}
{{{vehicles}}} 是否显示车型 可选 不適用 不適用 不適用
{{{image}}} 是否显示图片 可选 不適用 不適用 不適用

list 编辑

通过输入一个或多个线路编号以生成包含这些线路资料的表格。目前支持起讫点、线路方向、营运公司(分公司)、票价、运营时间、车辆图片、线路配车、BRT站台信息、备注等信息。

{{#invoke:CNBUS |list |city=#包含系统 }}

编号线路及运营时间收费运营商备注
1芳村花园南门
6:00–22:30
东山署前路
6:00–22:30
2元一汽一分芳村客运站

或者,线路也可以使用单个模式匹配表达式(必须以^开头)指定,用例参见重庆公交线路列表 (中心城区)。常用的代码匹配方式如:

  • ^T:所有T开头的线路;
  • ^%d%d%D*$:所有两位数的线路(允许非数字后缀,不允许更多数字);
  • ^1%d%d%D*$:1开头三位数的线路(允许非数字后缀,不允许更多数字)。

注意,输出的线路将按照线路代码以字符串排序,这意味着2会排在10的后面;此情形下建议配合{{{start}}}{{{end}}}将不同位数代码分拆多个表格显示。

collapsibleList 编辑

类似list,但只会生成一个的简化版的表格。目前仅支持起讫点和线路方向。

{{#invoke:CNBUS |collapsibleList |city=#包含系统 }}

color 编辑

属于辅助功能,可输出代表线路运营商的颜色代号。list 已集成该功能。

lineName 编辑

展示简短行内链接。如{{惠州巴士路线极简列表|1}}:公交路线:1

数据格式 编辑

本模块约定将数据存储在子模块中。

城市总表模块 编辑

在将本模块引入公交系统前,首先需要建立一个子模块作为该系统的数据模块。请将该子模块命名为Module:CNBUS/<城市代码>,其基本框架为:

local xx = {
    areas = { },
    operators = { }
}

xx.areas['xx'] = {
    name = "<区域名>",
    page = "<线路列表条目名>",
    source = "Module:CNBUS/XX/data", -- 对应模块
    aliases = { "XX", "理塘", "default" } -- 
}

xx.operators['bus'] = {
    color = "red",
    aliases = { "Bus", "公交集团" }
}
xx.operators['transport'] = {
    color = "silver",
    aliases = { "交运集团" }
}

return xx

区域表 编辑

每个子模块可包含一个或多个子区域,可分别存放城区、郊区、外市路线的资料。在本模块设置各区域资料的例子如下:

gz.areas['guangzhou'] = {
	name = "广州",
	page = "广州巴士路线列表",
	source = "Module:CNBUS/GZ/data",
	aliases = { "Guangzhou", "GZ", "gz", "广州", "廣州", "default" }
}
gz.areas['nansha'] = {
	name = "南沙",
	page = "南沙巴士路线列表",
	source = "Module:CNBUS/GZ/nansha",
	aliases = { "Nansha", "NS", "ns", "南沙" }
}
gz.areas['foshan'] = {
	name = "佛山",
	page = "佛山巴士路线列表",
	source = "Module:CNBUS/GZ/foshan",
	aliases = { "Foshan", "FS", "fs", "佛山" }
}

其中,source的值为子区域各线路的详细资料;pagename用于设置list和collapsibleList的标题中指向列表条目的内部链接([[page|name]],如[[广州巴士路线列表|广州]]);aliases 则包含了该子区域的别名,由area参数调用。

本例中共有3个区域,分别为“guangzhou”、“nansha”和“foshan”。在未提供area参数的值,或area参数的值为“guangzhou”、“Guangzhou”、“GZ”、“gz”、“广州”、“廣州”时,则选择区域“guangzhou”。“nansha”和“foshan”同理。

运营商表 编辑

在本模块设置各运营商颜色的例子如下:

xx.operators['bus1'] = {
    color = "orange", -- 颜色
    aliases = { "一汽一分", "一汽二分" } -- 别名。键值本身(此处为bus1)不需要包含其中
}
xx.operators['bus3'] = {
    color = "#fff600",
    aliases = { "三汽一分", "三汽二分" }
}
xx.operators['other'] = {
    color = "white",
}
xx.operators['multi'] = {
    color = "black",
}

在本例中,一汽和三汽公司的代码分别为bus1bus3,则运营商为“一汽一分”和“一汽二分”的代表色为“orange”(橙色),“三汽一分”和“三汽二分”的代表色为“#fff600”(近似于黄色)。此外,还需要设置“other”和“multi”,分别代表模块中未列出的运营商(显示为白色)和多于一个运营商(显示为黑色)。

线路表模块 编辑

此后,便是为各个区域添加具体的线路(line)资料。请在该子模块下再新建一个二级子模块,并命名为“Module:CNBUS/<城市代码>/<区域代码>”,其基本框架如下:

local p = {
-- 常规线路
['1'] = { name = "线路名", mark = "线路名标注", fare = "票价", operators = "运营商", vehicles = { "配车1", "..." }, note = "备注", image = "[[File:示例.jpg|128px]]",
	{ { "左起讫点", time = "发车时间" }, { "方向箭头", mark = "方向标注" }, { "右起讫点", time = "发车时间" } }, --[[区间1]]
	{ { "左起讫点", time = "发车时间" }, { "方向箭头", mark = "方向标注" }, { "右起讫点", time = "发车时间" } }, --[[区间2]]
	{ --[[…]] }, --[[任意数量区间]] },
-- BRT线路
['B1'] = { name = "线路名", mark = "线路名标注", fare = "票价" --[[BRT样式下不可见]], operator = "运营商", note = "备注", image = "[[File:图片.jpg|x128px]]", brt = { { "驶入BRT通道车站", "经停BRT车站数", "驶出BRT通道车站" --[[右向]] }, { "驶出BRT通道车站", "经停BRT车站数", "驶入BRT通道车站" --[[左向车站定义位置相反]] } }
	{ { "左起讫点", time = "发车时间" }, { "→" --[[完整列表BRT样式下由brt字段自动确定;单向线路需要为其他情况填写]], mark = "方向标注" }, { "右起讫点", time = "发车时间" } }, },

-- 停办线路
['114'] = { name = "线路名", mark = "线路名标注", status = { -1, date = "日期" } },
-- 暂时停运线路
['514'] = { name = "线路名", mark = "线路名标注", status = { 0, date = "日期" } },
}

-- 导入其他模块的线路资料。请注意,引用的线路代码不能是重定向
p._external = {
	['Module:CNBUS/XX/data'] = {
    	['1'] = '1',
        ['114'] = { '514', override = { name = '114' } }, -- 支持覆写部分属性
    },
	['Module:CNBUS/YY/data'] = {
    	['2'] = '2',
    },
}

-- 定义线路编号重定向
p._map = {
	['01'] = '1',
	['BRT1'] = 'B1',
}

return p
  • 线路代码['line']区分简体/繁体和英文大写/小写的,因此在使用时不可混用,建议统一同城市下各子系统的简繁和大小写规则。其他参数内容不受限制。
  • 如需在名词中使用连字符,请使用“-”而非“-”或“-”等。为使用方便,连字暨减号-U+002D)在线路名称、时间、票价和备注中,与数字字母相邻时将被替换为半宽连接号U+2013),其余情形下将被替换为全宽连接号U+2014)。
  • name必填,<区间>[1][1]<区间>[2][1]<区间>[3][1]fareoperatornote参数建议填写(<区间>相关补全规则见#区间子表)。
  • operatorsvehicles既可以是字符串,也可以是数组(显示时分行)。数组表示的operators会直接被视为多运营商。
  • 若需要显示BRT信息,则右向<BRT>[1][1]<BRT>[1][2]<BRT>[1][3]和左向<BRT>[2][1]<BRT>[2][2]<BRT>[2][3]参数需要至少填入一组。右向为空时需要显式置nil方能填写左向数据;若实际存在但缺少相关资料,则应将对应方向置为空表{ }
  • 各参数值可以加入内部链接及换行符<br />,但来源引用<ref>和各类模板是无法使用的。

区间子表 编辑

为使用简便,一部分置空的值会使用邻近的值进行取代。补全的顺序遵循区间定义的顺序,区间内部则依次为左、右、方向。首个区间(route)和后继区间的回落规则有所不同。

对于每条线路的首个区间

  • 左起讫点保持原样(as-is),除表不存在(nil或未定义)时转化为空表;
  • 右起讫点若无效(表不存在;表首位的字符串不存在或长度为0),复制左起讫点的名称,附属属性保持原样;
  • 方向若无效(表不存在;表首位的字符串不存在或长度为0),左右起讫点(回落补全后)若相等则视作逆时针循环线,否则视为往返线,附属属性保持原样。
{ },
-- 等价于
{ { }, { "↺" }, { } },

{ { "火车站" }, { mark = "直" }, { "机场" } },
-- 等价于
{ { "火车站" }, { "⇆", mark = "直" }, { "机场" } },

{ { "火车站", time = "10:00" }, nil, { time = "20:00" } },
-- 等价于
{ { "火车站", time = "10:00" }, { "↺" }, { "火车站", time = "20:00" } },

对于后继区间

  • 左右起讫点若表不存在,则复制上一区间对应起讫点的所有属性;若表首位的字符串(名称)不存在或长度为0,则只复制上一区间对应起讫点的名称,附属属性保持原样;
  • 方向的行为与首个区间相同。
-- 假定补全后的上一区间
{ { "火车站", time = "10:00" }, { "⇆", mark = "快" }, { "机场", time = "20:00" } },

{ },
-- 等价于
{ { "火车站", time = "10:00" }, { "⇆" }, { "机场", time = "20:00" } },

{ nil, { mark = "直" }, { "" } },
-- 等价于
{ { "火车站", time = "10:00" }, { "⇆", mark = "直" }, { "机场" } },

用例 编辑

常见错误 编辑

在条目中使用引用了本模块的模板后,可能会提示以下错误:

错误提示 错误原因 解决方法
错误 Module:CNBUS不存在“XXX”的公交系统数据 Module:CNBUS/XXX不存在 检查模板中 city=<城市代码> 的<城市代码>是否填写错误
错误 “city”参数为空,请输入城市代码 模板未填入 city 的值 填写模板中 city=<城市代码> 的<城市代码>
错误 Module:CNBUS/XXX中未包含“yyy”的资料模块 Module:CNBUS/XXX/yyy不存在 检查模板中 loc=<子系统代码> 的<子系统代码>是否填写错误
错误 资料模块Module:CNBUS/XXX/yyy出现错误,请前往检查 Module:CNBUS/XXX/yyy出现错误 大多为资料模块导入其他模块时输入错误(本说明文档示例中的“导入其他模块的线路资料”部分),请仔细检查资料模块中该部分代码是否有误。如无法定位错误,可借助编辑框下方的“调试控制台”寻找出错行数。不知道怎么用?最简单的方法是输入 print(p) 后回车,如提示“Lua错误”,即可找到出错的变量及其位置。
无输入 请输入线路编号 使用模板时填入线路编号,或去除多余的“|”
1234 本线已于0202年1月1日停办,请移除 使用模板时删除该线路
5678 本线自0202年1月1日起暂停服务 使用模板时视情况删除或保留该线路
9012 本线并非BRT线路 线路缺少 brt_b 参数 如该线路并非BRT线路,使用模板时请勿选择 style=BRT 样式;如该线路的确为BRT线路,请补充完整该线路在BRT通道的行驶信息,否则使用模板时请勿选择 style=BRT 样式。

备注 编辑

本模块的相关功能以先前广州巴士路线列表的样式排版设计。模块前身为{{廣州巴士路線}};为优化页面加载速度和方便在不同系统间调用资料,模板于2019年中进行模块化改版(Module:GZBUS);为方便各模块的维护管理,2020年5月再将原先各模块合并于此。

如您在使用本模块时遇到问题或有任何建议,欢迎在模块讨论页中提出。

local p = {}

local obsolete = '[[Category:调用CNBUS废弃接口的页面]]'

---@overload fun(s: string): boolean?
---@overload fun(s: string, default: boolean): boolean
local yesno = require('Module:yesno')

---@param var any
---@return boolean
local function isEmpty(var)
	return not var or var == ''
end

---将空字符串转换为`nil`
---@param var string?
---@return string?
local function nilEmpty(var)
	if var == '' then return nil
	else return var
	end
end

--#region 线路定义

---旧版线路
---@class metalineLegacy
---@field code string
---@field endpoint1 string
---@field time1 string?
---@field direction string?
---@field endpoint2 string
---@field time2 string?
---@field direction1 string?
---@field endpoint3 string?
---@field time3 string?
---@field fare string
---@field company string
---@field image string?
---@field vehicle string?
---@field brt_a string?
---@field brt_a_in string?
---@field brt_a_out string?
---@field brt_b string?
---@field brt_b_in string?
---@field brt_b_out string?
---@field brt_info string?
---@field note string?
---@field suspend string?
local LL = {}

---转换旧版线路
---@return line
function LL:convert()
	---@type line
	local line = {
		{
			{ self.endpoint1, time = self.time1 },
			{ self.direction },
			{ self.endpoint2, time = self.time2 },
		},
		code = self.code,
		fare = self.fare,
		operator = self.company,
		image = self.image,
		vehicle = self.vehicle and mw.text.split(self.vehicle, '<br/>'),
		note = self.note,
	}
	if self.endpoint3 then
		table.insert(line, {
			{ self.endpoint1, time = self.time1 },
			{ self.direction1 },
			{ self.endpoint3, time = self.time3 },
		})
	end
	if self.brt_a or self.brt_b then
		line.brt = { info = self.brt_info }
		if self.brt_a then
			line.brt[1] = {
				self.brt_a,
				i = self.brt_a_in,
				o = self.brt_a_out,
			}
		end
		if self.brt_b then
			line.brt[2] = {
				self.brt_b,
				i = self.brt_b_in,
				o = self.brt_b_out,
			}
		end
	end
	if line.note and mw.ustring.match(line.note, '已停[办辦]') then
		line.status = { -1, date = self.suspend }
	elseif line.note and line.note == '暂停服务' or line.note == '暂停服务' then
		line.status = { 0, date = self.suspend }
	end
	return line
end

---@alias terminus { [1]: string?, time: string? }
---@alias direction { [1]: string?, mark: string? }
---@alias route { [1]: terminus?, [2]: direction?, [3]: terminus? }
---@alias pTerminus { [1]: string?, time: string?, rowspan: integer? }
---@alias pRoute { [1]: pTerminus, [2]: direction, [3]: pTerminus }

---@alias brt { [1]: string, i: string, o: string }

---@class line: { [number]: route }
---@field code string
---@field mark string?
---@field fare string
---@field operator string
---@field image string?
---@field vehicle string[]?
---@field brt { [1]: brt?, [2]: brt?, info: string? }?
---@field note string?
---@field status { [1]: integer, date: string }?
local L = {}

---获取解析区间表
---@return pRoute[]
function L:getParsedRoutes()
	---@type pRoute[]
	local routes = {}
	local last_route = { { }, nil, { } } -- 引用上一区间/交路
	for r, route in ipairs(self) do
		local el = isEmpty(route[1] and route[1][1]) and r > 1
		local ed = isEmpty(route[2] and route[2][1])
		local er = isEmpty(route[3] and route[3][1])
		if el or ed or er then
			---@type terminus
			local left, right

			-- 当终点站表整体悬空时,克隆所有属性
			if not route[1] then
				left = last_route[1]
			-- 否则,只复制站名
			else
				left = {
					nilEmpty(route[1][1]) or last_route[1][1],
					time = route[1].time
				}
			end

			-- 同上,但首行右终点将回落左终点
			if not route[3] then
				right = r == 1 and left or last_route[3]
			else
				right = {
					nilEmpty(route[3][1]) or (r == 1 and left[1] or last_route[3][1]),
					time = route[3].time
				}
			end

			-- 隐式方向
			local direction = { ed and ((left[1] == right[1]) and '↺' or '⇆') or route[2] }

			table.insert(routes, { left, direction, right, note = self.note })
		else
			table.insert(routes, route)
		end
	end

	if #routes > 1 then
		for r = #routes, 2, -1 do
			if nilEmpty(routes[r][1][1]) == nilEmpty(routes[r - 1][1][1]) and
			   nilEmpty(routes[r][1].time) == nilEmpty(routes[r - 1][1].time) then
				routes[r - 1][1].rowspan = (routes[r][1].rowspan or 1) + 1
				routes[r][1].rowspan = 0
			end
			if nilEmpty(routes[r][3][1]) == nilEmpty(routes[r - 1][3][1]) and
			   nilEmpty(routes[r][3].time) == nilEmpty(routes[r - 1][3].time) then
				routes[r - 1][3].rowspan = (routes[r][3].rowspan or 1) + 1
				routes[r][3].rowspan = 0
			end
		end
	end

	return routes
end

--#endregion

--#region 区域定义

---@alias mArea { name: string, page: string, source: string, aliases: string[] }

---@class area
---@field name string
---@field page string
---@field source string
---@field aliases string
---@field lines { [string]: line }
local A = {}

---获取线路 w/ err
---@param l string
---@param inline boolean?
---@return line
---@return string?
function A:getLine(l, inline)
	local line = self.lines[l]
	---@diagnostic disable-next-line: undefined-field
	if line and (line.endpoint1 or line.note == '已停办' or line.note == '已停辦' or line.note == '暂停服务' or line.note == '暂停服务') then
		---@diagnostic disable-next-line: param-type-mismatch
		line = LL.convert(line)
	end

	local err = nil
	if inline then
		if isEmpty(l) then
			err = '未输入线路'
		elseif not line then
			err = string.format('[[%s]]中无%s线路', self.source, line.code)
		elseif line.status then
			if line.status[1] == -1 then
				if isEmpty(line.status.date) then
					err = string.format('%s已停办', line.code)
				else
					err = string.format('%s已于%s停办', line.code, line.status.date)
				end
			elseif line.status[1] == 0 then
				if isEmpty(line.status.date) then
					err = string.format('%s已暂停服务', line.code)
				else
					err = string.format('%s自%s起暂停服务', line.code, line.status.date)
				end
			end
		end
	else
		if isEmpty(l) then
			err = string.format('请输入线路[[%s巴士路线列表|编号]]', self.name)
		elseif not line then
			err = string.format('[[%s]]中未包含这条[[%s巴士路线列表|%s]]的线路', self.source, self.name, self.name)
		elseif line.status then
			if line.status[1] == -1 then
				if isEmpty(line.status.date) then
					err = '本线已停办,请移除'
				else
					err = string.format('本线已于%s停办,请移除', line.status.date)
				end
			elseif line.status[1] == 0 then
				if isEmpty(line.status.date) then
					err = '本线已暂停服务'
				else
					err = string.format('本线自%s起暂停服务', line.status.date)
				end
			end
		end
	end

	return line, err
end

--#endregion

--#region 城市定义

---@alias rmap { [string]: string[] }
---@alias qmap { [string]: string }
---@alias mCityLegacy { colors: qmap, company: rmap, data: qmap, titlename: qmap, listname: qmap, location: rmap }
---@alias mCity { areas: { [string]: mArea }, operators: { [string]: operator } }

---@alias operator { color: string, aliases: string[] }

---@class city
---@field areas { [string]: area }
---@field area_map { [string]: string }
---@field lines { [string]: { [string]: line } }
---@field operators { [string]: operator }
---@field operator_map { [string]: string }
local data = {}

---@param a string
---@return area
function data:getArea(a)
	return self.areas[a] or self.areas[self.area_map[a]] or self.areas['default']
end

---@param o string
---@return operator
function data:getOperator(o)
	return self.operators[o] or self.operators[self.operator_map[o]]
end

--#endregion

--#region 数据模块

---导入城市数据
---@param c string
local function loadCityData(c)
	if not data.areas then
		if isEmpty(c) then
			error(string.format('“city”参数为空,请输入城市代码'))
		end

		local success, ro_data = pcall(mw.loadData, 'Module:CNBUS/' .. c)
		if not success then
			error(string.format('[[Module:CNBUS]]不存在“%s”的公交系统数据', c))
		end

		-- 引用只读数据
		if ro_data.data then
			---@cast ro_data mCityLegacy
			data.area_map = {}
			data.areas = {}
			data.operator_map = {}
			data.operators = {}

			for a, source in pairs(ro_data.data) do
				data.areas[a] = { source = source }
			end

			for a, name in pairs(ro_data.titlename or {}) do
				data.areas[a].name = name
			end

			for a, page in pairs(ro_data.listname or {}) do
				data.areas[a].page = page
			end

			for a, aliases in pairs(ro_data.location or {}) do
				for _, alias in ipairs(aliases) do
					data.area_map[alias] = a
				end
			end

			for o, color in pairs(ro_data.colors) do
				data.operators[o] = { color = color }
			end

			for o, aliases in pairs(ro_data.company or {}) do
				for _, alias in ipairs(aliases) do
					data.operator_map[alias] = o
				end
			end
		else
			---@cast ro_data mCity
			data.area_map = {}
			data.operator_map = {}

			for a, ro_area in ro_data.areas do
				data.areas[a] = setmetatable({}, { __index = ro_area })

				for _, alias in ipairs(ro_area.aliases) do
					data.area_map[alias] = a
				end
			end

			data.operators = setmetatable({}, { __index = ro_data.operators })
			for o, operator in pairs(data.operators) do
				for _, alias in ipairs(operator.aliases) do
					data.operator_map[alias] = o
				end
			end
		end
	end
end

---导入区域线路数据
---@param c string
---@param a string
local function loadAreaData(c, a)
	loadCityData(c)

	if isEmpty(a) then
		error(string.format('“area”参数为空,请输入区域代码'))
	end

	local area = data:getArea(a)

	if not area then
		error(string.format('[[Module:CNBUS/%s]]中未包含“%s”的资料模块', c, a))
	end

	if not area.lines then
		local success, ro_data = pcall(mw.loadData, area.source)
		if not success then
			error(string.format('资料模块[[%s]]出现错误,请前往检查', area.source))
		end

		area.lines = ro_data
	end
end

--#endregion

--#region 颜色模板

---@param city string
---@param operator string?
---@return string
function p._color(city, operator)
	loadCityData(city)

	local info = data:getOperator(operator or 'other')
	if info then
		return info.color
	-- 运营商名超过6字(UTF-8下18字节)视为联营
	elseif (operator and string.len(operator) > 18) or operator == 'multi' then
		return data:getOperator('multi').color
	else
		return data:getOperator('other').color
	end
end

---运营商颜色
---@param frame frame
---@return string
function p.color(frame)
	local a = frame.args
	return p._color(a.city, a.operator or a.company)
end

---@param city string
---@param operator string
---@deprecated
function p._colorBox(city, operator)
	return string.format('width=1%% bgcolor=%s', p._color(city, operator))
end

---{{巴士公司色块}}
---@param frame frame
---@return string
---@deprecated
function p.colorbox(frame)
	local a = frame.args
	return p._colorBox(a.city, a.operator or a.company) -- .. obsolete
end

--#endregion

--#region 列表公共模板

local fdash = mw.ustring.char(0x2012)

---@param s string
---@return string
local function fixFigureDash(s)
	s, _ = mw.ustring.gsub(s, '-', fdash)
	return s
end

---@param color string
---@param rowspan integer?
---@return html?
local function makeBarCell(color, rowspan)
	local td = mw.html.create('td')
		:addClass('bar')
		:css('background-color', color)

	if rowspan and rowspan > 1 then
		td	:attr('rowspan', rowspan)
	end

	return td:allDone()
end

---@param line line
---@param rowspan integer?
---@return html?
local function makeCodeCell(line, rowspan)
	local td = mw.html.create('td')
		:addClass('code')
		:wikitext(line.code)

	if rowspan and rowspan > 1 then
		td	:attr('rowspan', rowspan)
	end

	if nilEmpty(line.mark) then
		td:tag('br', { selfClosing = true }):done():tag('small'):wikitext(line.mark):done()
	end

	return td:allDone()
end

---@param route pRoute
---@param t 1|3 Left or right
---@param bTime boolean
---@param rowspan integer? Override
---@return html?
local function makeTerminusCell(route, t, bTime, rowspan)
	local terminus = route[t]
	local n_rows = rowspan or terminus.rowspan

	if n_rows == 0 then
		return nil
	end

	local td = mw.html.create('td')
	td	:addClass('terminus-' .. ((t == 1) and 'left' or 'right'))
		:wikitext(terminus[1])

	if n_rows and n_rows > 1 then
		td	:attr('rowspan', n_rows)
	end

	if bTime and nilEmpty(terminus.time) then
		td	:tag('br', { selfClosing = true }):done()
			:tag('small'):wikitext(fixFigureDash(terminus.time)):done()
	end

	return td:allDone()
end

---@param route pRoute
---@param direction string? override
---@return html?
local function makeDirectionCell(route, direction)
	local td = mw.html.create('td')
		:addClass('direction')

	if not direction and nilEmpty(route[2].mark) then
		td:tag('small'):wikitext(route[2].mark):done():tag('br', { selfClosing = true }):done()
	end

	return td:wikitext(direction or route[2][1]):allDone()
end

---@param prop string
---@param rowspan integer?
---@param dash boolean?
local function makePropCell(prop, rowspan, dash)
	local td = mw.html.create('td')

	if dash then
		td:wikitext(fixFigureDash(prop))
	else
		td:wikitext(prop)
	end

	if rowspan and rowspan > 1 then
		td	:attr('rowspan', rowspan)
	end

	return td:allDone()
end

---@param line line
---@param bImage boolean
---@param rowspan integer?
---@return html?
local function makeNoteCell(line, bImage, rowspan)
	local td = mw.html.create('td')

	if bImage then
		local sep = (nilEmpty(line.note) or nilEmpty(line.image)) and '<br/>' or ''

		td	:addClass('note')
			:wikitext(line.note, sep, line.image)
			:done()
	else
		td	:addClass('note')
			:wikitext(line.note)
			:done()
	end

	if rowspan and rowspan > 1 then
		td	:attr('rowspan', rowspan)
	end

	return td:allDone()
end

--#endregion

--#region 列表模板

---@alias listFlags { brt: boolean, time: boolean, fare: boolean, operator: boolean, vehicle: boolean, image: boolean }

---解析列表标志位
---@param typ string
---@param fTime string
---@param fVehicle string
---@param fImage string
---@return listFlags flags
local function parseListFlags(typ, fTime, fVehicle, fImage)
	typ = mw.ustring.lower(typ)
	if mw.ustring.find(typ, 'brt') then
		return {
			brt      = true,
			time     = yesno(fTime, false),
			fare     = false,
			operator = false,
			vehicle  = false,
			image    = yesno(fImage, false),
		}
	else
		return {
			brt      = false,
			time     = yesno(fTime, false),
			fare     = not mw.ustring.match(typ, 'no%w*fa'),
			operator = not mw.ustring.match(typ, 'no%w*co'),
			vehicle  = yesno(fVehicle, false),
			image    = yesno(fImage, false),
		}
	end
end

---获取行CSS类
---@param f listFlags
---@return string
local function getListRowClass(f)
	if f.brt then
		return 'cnbus-brt'
	else
		return 'cnbus-l' .. ((f.fare and 1 or 0) + (f.operator and 1 or 0) + (f.vehicle and 2 or 0))
	end
end

---@param c string
---@param a string
---@param f listFlags
---@param class string
---@return html head
function p._listHead(c, a, f, class)
	local success, err = pcall(loadAreaData, c, a)

	if not success then
		if f.brt then
			return mw.html.create('tr')
				:tag('th'):attr('colspan', 10):wikitext(err)
				:allDone()
		else
			local n_cols = 6 + (f.fare and 1 or 0) + (f.operator and 1 or 0) + (f.vehicle and 1 or 0)
			return mw.html.create('tr')
				:tag('th'):attr('colspan', n_cols):wikitext(err)
				:allDone()
		end
	end

	local area = data:getArea(a)
	local header_lines = (f.time and "线路及运营时间") or "线路"
	local header_note = (f.image and "图片及备注") or "备注"
	local link_page = area.page or string.format('%s巴士路线列表', area.name)

	if f.brt then
		return mw.html.create('tr')
			:addClass(class)
			:tag('th'):attr('colspan', 2):wikitext(string.format('[[%s|编号]]',link_page)):done()
			:tag('th'):attr('colspan', 3):wikitext(header_lines):done()
			:tag('th'):wikitext('驶入BRT通道'):addClass('inout'):done()
			:tag('th'):wikitext('驶出BRT通道'):addClass('inout'):done()
			:tag('th'):wikitext('BRT通道停靠站数'):addClass('count'):done()
			:tag('th'):wikitext('运营商'):addClass('operator'):done()
			:tag('th'):wikitext(header_note):addClass('note'):done()
			:allDone()
	else
		local tr = mw.html.create('tr'):addClass(class)

		tr	:tag('th'):attr('colspan', 2):wikitext(string.format('[[%s|编号]]', link_page)):done()
			:tag('th'):attr('colspan', 3):wikitext(header_lines):done()
		if f.fare then
			tr:tag('th'):wikitext('收费'):addClass('fare'):done()
		end
		if f.operator then
			tr:tag('th'):wikitext('运营商'):addClass('operator'):done()
		end
		if f.vehicle then
			tr:tag('th'):wikitext('运力'):addClass('vehicle'):done()
		end
		tr:tag('th'):wikitext(header_note):addClass('note'):done()

		return tr:allDone()
	end
end

---表头模板
---@param frame frame
---@return html|string
---@deprecated
function p.title(frame)
	local a = frame.args
	local flags = parseListFlags(
		a.type or a.format or '',
		a.time,
		a.vehicle,
		a.image)
	local result = p._listHead(
		a.city,
		a.area or a.loc,
		flags,
		getListRowClass(flags))
	local output = mw.html.create('')
	return output:node(result):wikitext(obsolete):newline():allDone()
end

---@param c string
---@param a string
---@param l string
---@param f listFlags
---@param class string
---@return html head
function p._listRow(c, a, l, f, class)
	local success
	local err
	success, err = pcall(loadAreaData, c, a)

	local output = mw.html.create()

	if not success then
		l = '错误'
	else
		local area = data:getArea(a)
		local line
		line, err = A.getLine(area, l)

		if not err then
			local color = p._color(c, line.operator)

			-- BRT线路(广州、中山)
			if f.brt then
				if not line.brt then
					err = '本线并非[[快速公交系统|BRT线路]]'
				else
					local route = line[1]

					if (line.brt[1] and line.brt[2]) then
						output
							:tag('tr')
								:addClass(class)
								:addClass('line')
								:node(makeBarCell(color, 2))
								:node(makeCodeCell(line, 2))
								:node(makeTerminusCell(route, 1, f.time, 2))
								:node(makeDirectionCell(route, '→'))
								:node(makeTerminusCell(route, 3, f.time, 2))
								:node(makePropCell(line.brt[1].i))
								:node(makePropCell(line.brt[1].o))
								:node(makePropCell(line.brt[1][1]))
								:node(makePropCell(line.operator, 2))
								:node(makeNoteCell(line, f.time, 2))
								:done()
							:tag('tr')
								:addClass('route')
								:node(makeDirectionCell(route, '←'))
								:node(makePropCell(line.brt[2].i))
								:node(makePropCell(line.brt[2].o))
								:node(makePropCell(line.brt[2][1]))
								:done()
					else
						local info = line.brt[1] or line.brt[2] --[[@as brt]]

						output
							:tag('tr')
								:addClass(class)
								:addClass('line')
								:node(makeBarCell(color, 1))
								:node(makeCodeCell(line, 1))
								:node(makeTerminusCell(route, 1, f.time, 1))
								:node(makeDirectionCell(route))
								:node(makeTerminusCell(route, 3, f.time, 1))
								:node(makePropCell(info.i))
								:node(makePropCell(info.o))
								:node(makePropCell(tostring(info[1])))
								:node(makePropCell(line.operator))
								:node(makeNoteCell(line, f.image))
								:done()
					end
				end
			-- 常规线路
			else
				local routes = L.getParsedRoutes(line)
				local tr

				for r, route in ipairs(routes) do
					tr = mw.html.create('tr')

					if r == 1 then
						tr	:addClass(class)
							:addClass('line')
							:node(makeBarCell(color, #routes))
							:node(makeCodeCell(line, #routes))
					else
						tr	:addClass('route')
					end

					tr	:node(makeTerminusCell(route, 1, f.time))
						:node(makeDirectionCell(route))
						:node(makeTerminusCell(route, 3, f.time))

					if r == 1 then
						if f.fare then
							tr:node(makePropCell(line.fare, #routes, true))
						end

						if f.operator then
							tr:node(makePropCell(line.operator, #routes))
						end

						if f.vehicle then
							tr:node(makePropCell(table.concat(line.vehicle, '<br/>'), #routes))
						end

						tr:node(makeNoteCell(line, f.image, #routes))
					end

					output:node(tr:allDone())
				end
			end
		end

		if err then
			local n_cols
			if f.brt then
				n_cols = 8
			else
				n_cols = 4 + (f.fare and 1 or 0) + (f.operator and 1 or 0) + (f.vehicle and 1 or 0)
			end

			output:tag('tr'):addClass('msg')
				:tag('td'):addClass('bar'):done()
				:tag('td'):addClass('code')
					:wikitext((line and line.code) or l)
					:done()
				:tag('td'):addClass('msg'):attr('colspan', n_cols)
					:wikitext(err)
					:done()
		end
	end

	return output:allDone()
end

---列表模板
---@param frame frame
---@return string
function p.list(frame)
	local a = frame.args

	if a.code then
		---@deprecated
		return obsolete
	else
		local open = yesno(a.open, true)
		local close = yesno(a.close, true)

		---@overload fun(frame: frame, options: table?): { [any]: string }
		local getArgs = require('Module:Arguments').getArgs
		a = getArgs(frame)
		local flags = parseListFlags(
			a.type or '',
			a.time,
			a.vehicle,
			a.image)
		local class = getListRowClass(flags)

		local rows = {}
		local prepend = ''
		if open then
			prepend = '<table class="wikitable sortable cnbus-normal">'
			table.insert(
				rows,
				tostring(
					p._listHead(
						a.city,
						a.area,
						flags,
						class)))
		end

		for _, l in ipairs(a) do
			table.insert(
				rows,
				tostring(
					p._listRow(
						a.city,
						a.area,
						l,
						flags,
						class)))
		end

		local append = close and '</table>' or ''

		return prepend .. table.concat(rows) .. append
	end
end

--#endregion

--#region 折叠列表模板

---@param c string
---@param a string
---@param l string
---@return html head
function p._collapsibleListRow(c, a, l)
	local success
	local err
	success, err = pcall(loadAreaData, c, a)

	local output = mw.html.create()

	if not success then
		l = '错误'
	end

	if not err then
		local area = data:getArea(a)
		local line
		line, err = A.getLine(area, l)

		if err then
			output:tag('tr')
				:tag('td'):wikitext(l):done()
				:tag('td'):attr('colspan', 4):wikitext(err):done()
			:done()
		else
			local routes = L.getParsedRoutes(line)

			for r, route in ipairs(routes) do
				local tr = mw.html.create('tr')

				if r == 1 then
					tr:node(makeCodeCell(line, #routes))
				end

				tr	:node(makeTerminusCell(route, 1, false))
					:node(makeDirectionCell(route))
					:node(makeTerminusCell(route, 3, false))

				if r == 1 then
					tr:node(makeNoteCell(line, false, #routes))
				end

				output:node(tr:allDone())
			end
		end
	end

	return output:allDone()
end

---折叠列表模板
---@param frame frame
---@return html|string
function p.collapsibleListRows(frame)
	---@overload fun(frame: frame, options: table?): { [any]: string }
	local getArgs = require('Module:Arguments').getArgs
	local a = getArgs(frame)

	local rows = {}
	for _, l in ipairs(a) do
		table.insert(rows, p._collapsibleListRow(a.city, a.area, l))
	end

	return table.concat(rows)
end

---简单列表单行模板(兼容中)
---@param frame frame
---@return html|string
---@deprecated
function p.simplelist(frame)
	local c = frame.args.city
	local a = frame.args.loc
	local l = frame.args.code

	local success
	local err
	success, err = pcall(loadAreaData, c, a)

	if not success then
		l = '错误'
	end

	if not err then
		local area = data:getArea(a)
		local line
		line, err = A.getLine(area, l)

		if not err then
			---@type string
			local dir, ep1, ep2
			ep1 = line[1][1][1]
			if isEmpty(line[1][3][1]) then
				ep2 = ep1								-- 未填入endpoint2默认为循环线
			else
				ep2 = line[1][3][1]
			end
			if isEmpty(line[1][2][1]) then					-- 部分线路省略了方向
				if isEmpty(line[1][3][1]) then				-- 未填入endpoint2默认为循环线
					dir = "↺"
				else
					dir = "⇆"
				end
			elseif line[2] and nilEmpty(line[2][2][1]) then			-- 部分线路的第二个方向
				if nilEmpty(line[2][3][1]) then			-- 部分线路去程终点和返程起点不同
					dir = line[1][2][1]..'<hr>'..line[2][2][1]
					ep2 = ep2..'<hr>'..line[2][3][1]
				else
					dir = line[1][2][1]..'<br>'..line[2][2][1]
				end
			else
				dir = line[1][2][1]
			end
			local data_note = line.note or ''
			l = line.code

			return string.format(
				[[|align=right|'''%s'''||align=right|%s||align=center nowrap=true|%s||%s||%s]],
				l,
				ep1,
				dir,
				ep2,
				data_note) .. obsolete
		end
	end

	return string.format(
		[[|align=right|'''%s'''||colspan=3 align=center|%s]],
		l,
		err) .. obsolete
end

--#endregion

--#region 紧凑列表模板

--#endregion

--#region 线路编号模板(惠州)

---@param c string
---@param a string
---@param l string
---@return string
function p._code(c, a, l)
	local success
	local err
	success, err = pcall(loadAreaData, c, a)

	if not success then
		l = '错误'
	end

	if not err then
		local area = data:getArea(a)
		local line
		line, err = A.getLine(area, l)

		if not err then
			return line.code or l
		end
	end

	return string.format('(%s)', err)
end

---线路编号模板
---@param frame frame
---@return string
---@deprecated
function p.code(frame)
	local a = frame.args
	return p._code(
		a.city,
		a.area or a.loc,
		a.code)
end

--#endregion

return p