Модуль:ТранспортнаяСхема: различия между версиями
Tringoli (обсуждение | вклад) мНет описания правки |
Tringoli (обсуждение | вклад) мНет описания правки Метка: ручная отмена |
||
| (не показано 6 промежуточных версий этого же участника) | |||
| Строка 14: | Строка 14: | ||
end | end | ||
return args | return args | ||
end | |||
-- Безопасная очистка текста JSON | |||
local function cleanJsonText(rawText) | |||
rawText = mw.ustring.gsub(rawText, '<noinclude>.-</noinclude>', '') | |||
rawText = string.gsub(rawText, '<!%-%-.-%-%->', '') | |||
rawText = string.gsub(rawText, '\194\160', ' ') | |||
rawText = string.gsub(rawText, '\194\173', '') | |||
return mw.text.trim(rawText) | |||
end | end | ||
| Строка 19: | Строка 28: | ||
local function loadData(args) | local function loadData(args) | ||
local dataParam = args['данные'] or args[1] or 'Муроморского' | local dataParam = args['данные'] or args[1] or 'Муроморского' | ||
local | dataParam = mw.text.trim(dataParam) | ||
local cleanName = mw.ustring.gsub(dataParam, '^[Шш][Аа][Бб][Лл][Оо][Нн]:', '') | |||
cleanName = mw.ustring.gsub(cleanName, '^[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee]:', '') | |||
cleanName = mw.ustring.gsub(cleanName, '/data%.json$', '') | |||
cleanName = mw.ustring.gsub(cleanName, '%.json$', '') | |||
cleanName = mw.ustring.gsub(cleanName, '^[Ии]нтерактивная схема%s*', '') | |||
cleanName = mw.text.trim(cleanName) | |||
for _, title in ipairs(candidates) do | local candidates = {} | ||
local | local function addCandidate(title) | ||
if | if title and title ~= '' then | ||
for _, existing in ipairs(candidates) do | |||
if existing == title then return end | |||
end | |||
table.insert(candidates, title) | |||
end | |||
end | |||
addCandidate(dataParam) | |||
addCandidate('Шаблон:Интерактивная схема ' .. cleanName .. '/data.json') | |||
addCandidate('Шаблон:Интерактивная схема ' .. cleanName .. ' метрополитена/data.json') | |||
addCandidate('Шаблон:' .. cleanName .. '/data.json') | |||
addCandidate('Шаблон:' .. cleanName) | |||
addCandidate('Интерактивная схема ' .. cleanName .. '/data.json') | |||
addCandidate('Интерактивная схема ' .. cleanName .. ' метрополитена/data.json') | |||
addCandidate(cleanName .. '/data.json') | |||
addCandidate(cleanName) | |||
local jsonErrors = {} | |||
for _, pageTitle in ipairs(candidates) do | |||
local ok, data = pcall(mw.loadJsonData, pageTitle) | |||
if ok and type(data) == 'table' then | |||
return data | |||
end | |||
local titleObj = mw.title.new( | local titleObj = mw.title.new(pageTitle) | ||
if titleObj | if titleObj then | ||
local | local rawText = titleObj:getContent() | ||
if | if rawText and rawText ~= '' then | ||
rawText = cleanJsonText(rawText) | |||
local decOk, decoded = pcall(mw.text.jsonDecode, rawText) | |||
local | if decOk and type(decoded) == 'table' then | ||
if | return decoded | ||
else | |||
table.insert(jsonErrors, '[[' .. pageTitle .. ']] (' .. tostring(decoded) .. ')') | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
error('Страница данных | |||
if #jsonErrors > 0 then | |||
error('Ошибка в синтаксисе JSON:\n* ' .. table.concat(jsonErrors, '\n* ')) | |||
end | |||
error('Страница данных не найдена ни по одному из путей:\n* [[' .. table.concat(candidates, ']]\n* [[') .. ']]') | |||
end | end | ||
| Строка 52: | Строка 94: | ||
end | end | ||
-- Генерация Canvas схемы с | -- Генерация интерактивной Canvas-схемы с панорамированием и управлением | ||
function p.canvas(frame) | function p.canvas(frame) | ||
local args = getArgs(frame) | local args = getArgs(frame) | ||
local data = loadData(args) | local data = loadData(args) | ||
local origW = tonumber(args['ширина_оригинала'] or 850) or 850 | local origW = tonumber(args['ширина_оригинала'] or 850) or 850 | ||
local origH = tonumber(args['высота_оригинала'] or 620) or 620 | local origH = tonumber(args['высота_оригинала'] or 620) or 620 | ||
-- | -- Кадрирование | ||
local cropX, cropY, cropW, cropH = nil, nil, nil, nil | local cropX, cropY, cropW, cropH = nil, nil, nil, nil | ||
if args['кадрирование'] or args['crop'] then | if args['кадрирование'] or args['crop'] then | ||
| Строка 78: | Строка 119: | ||
end | end | ||
local targetW = cropW or origW | local targetW = cropW or origW | ||
local targetH = cropH or origH | local targetH = cropH or origH | ||
| Строка 85: | Строка 125: | ||
local scale = tonumber(args['масштаб'] or args['scale']) | local scale = tonumber(args['масштаб'] or args['scale']) | ||
if boxW and not scale then | local isScrollMode = (args['прокрутка'] == 'да' or args['скролл'] == 'да' or args['scroll'] == 'yes') | ||
if not isScrollMode and boxW and not scale then | |||
scale = boxW / targetW | scale = boxW / targetW | ||
elseif not scale then | elseif not scale then | ||
| Строка 94: | Строка 136: | ||
local finalBoxH = tonumber(args['высота_блока'] or args['высота_карточки'] or args['высота']) or (targetH * scale) | local finalBoxH = tonumber(args['высота_блока'] or args['высота_карточки'] or args['высота']) or (targetH * scale) | ||
-- Внешний контейнер- | -- Внешний контейнер-вьюпорт с поддержкой перемещения клавиатурой и скролла | ||
local wrapper = mw.html.create('div') | local wrapper = mw.html.create('div') | ||
:addClass('ts-transport-map-viewport') | :addClass('ts-transport-map-viewport') | ||
:attr('tabindex', '0') | |||
:attr('title', 'Кликните для управления стрелками клавиатуры (← ↑ → ↓) или скролла мышью') | |||
:css({ | :css({ | ||
['position'] = 'relative', | ['position'] = 'relative', | ||
| Строка 102: | Строка 146: | ||
['height'] = string.format('%.1fpx', finalBoxH), | ['height'] = string.format('%.1fpx', finalBoxH), | ||
['max-width'] = '100%', | ['max-width'] = '100%', | ||
['overflow'] = 'hidden', | ['overflow'] = isScrollMode and 'auto' or 'hidden', | ||
['border-radius'] = ' | ['scroll-behavior'] = 'smooth', | ||
['overscroll-behavior'] = 'contain', | |||
['-webkit-overflow-scrolling'] = 'touch', | |||
['border-radius'] = '10px', | |||
['border'] = args['без_рамки'] and 'none' or '1px solid #d0d7de', | ['border'] = args['без_рамки'] and 'none' or '1px solid #d0d7de', | ||
['margin'] = (args['прижатие'] == 'лево' and '0 auto 0 0') or (args['прижатие'] == 'право' and '0 0 0 auto') or '0 auto', | ['margin'] = (args['прижатие'] == 'лево' and '0 auto 0 0') or (args['прижатие'] == 'право' and '0 0 0 auto') or '0 auto', | ||
| Строка 109: | Строка 156: | ||
['background-image'] = 'radial-gradient(#e1e4e8 1px, transparent 1px)', | ['background-image'] = 'radial-gradient(#e1e4e8 1px, transparent 1px)', | ||
['background-size'] = '20px 20px', | ['background-size'] = '20px 20px', | ||
['box-shadow'] = args['без_тени'] and 'none' or '0 2px 8px rgba(0,0,0,0. | ['box-shadow'] = args['без_тени'] and 'none' or '0 2px 8px rgba(0,0,0,0.06)', | ||
['outline'] = 'none', | |||
['user-select'] = 'none' | ['user-select'] = 'none' | ||
}) | }) | ||
local shiftX = cropX or 0 | local shiftX = cropX or 0 | ||
local shiftY = cropY or 0 | local shiftY = cropY or 0 | ||
-- Внутренний | -- Внутренний холст схемы | ||
local canvas = wrapper:tag('div') | local canvas = wrapper:tag('div') | ||
:addClass('ts-html-transport-canvas') | :addClass('ts-html-transport-canvas') | ||
:css({ | :css({ | ||
['position'] = 'absolute', | ['position'] = isScrollMode and 'relative' or 'absolute', | ||
['left'] = string.format('%.1fpx', -shiftX * scale), | ['left'] = not isScrollMode and string.format('%.1fpx', -shiftX * scale) or '0', | ||
['top'] = string.format('%.1fpx', -shiftY * scale), | ['top'] = not isScrollMode and string.format('%.1fpx', -shiftY * scale) or '0', | ||
['width'] = origW .. 'px', | ['width'] = origW .. 'px', | ||
['height'] = origH .. 'px', | ['height'] = origH .. 'px', | ||
['transform'] = string.format('scale(%.4f)', scale), | ['transform'] = (scale ~= 1.0 and not isScrollMode) and string.format('scale(%.4f)', scale) or 'none', | ||
['transform-origin'] = '0 0', | ['transform-origin'] = '0 0', | ||
['font-family'] = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" | ['font-family'] = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" | ||
}) | }) | ||
-- Шапка/логотип | -- Шапка/логотип | ||
if not (args['шапка'] == 'нет' or cropX ~= nil) then | if not (args['шапка'] == 'нет' or cropX ~= nil) then | ||
local header = canvas:tag('div') | local header = canvas:tag('div') | ||
| Строка 163: | Строка 210: | ||
header:tag('div') | header:tag('div') | ||
:css({['text-align'] = 'left'}) | :css({['text-align'] = 'left'}) | ||
:tag('div'):css({['font-weight'] = 'bold', ['font-size'] = '13px', ['color'] = '#1f2328'}):wikitext(data.title or ' | :tag('div'):css({['font-weight'] = 'bold', ['font-size'] = '13px', ['color'] = '#1f2328'}):wikitext(data.title or 'Скоростной транспорт'):done() | ||
:tag('div'):css({['font-size'] = '10px', ['color'] = '#57606a', ['text-transform'] = 'uppercase', ['letter-spacing'] = '0.5px'}):wikitext('Интерактивная схема линий'):done() | :tag('div'):css({['font-size'] = '10px', ['color'] = '#57606a', ['text-transform'] = 'uppercase', ['letter-spacing'] = '0.5px'}):wikitext('Интерактивная схема линий'):done() | ||
end | |||
-- Индикатор подсказки управления | |||
if isScrollMode then | |||
wrapper:tag('div') | |||
:css({ | |||
['position'] = 'sticky', | |||
['top'] = '10px', | |||
['left'] = 'calc(100% - 130px)', | |||
['width'] = '115px', | |||
['z-index'] = '10', | |||
['background'] = 'rgba(255,255,255,0.88)', | |||
['border'] = '1px solid #d0d7de', | |||
['border-radius'] = '16px', | |||
['padding'] = '3px 8px', | |||
['font-size'] = '10px', | |||
['color'] = '#57606a', | |||
['display'] = 'flex', | |||
['align-items'] = 'center', | |||
['gap'] = '4px', | |||
['pointer-events'] = 'none', | |||
['box-shadow'] = '0 1px 4px rgba(0,0,0,0.05)', | |||
['backdrop-filter'] = 'blur(2px)' | |||
}) | |||
:wikitext('⌨ ⇄ ⇅ Скролл') | |||
end | end | ||
if data.lines then | if data.lines then | ||
-- 1. Слой путей | -- 1. Слой путей | ||
for _, line in ipairs(data.lines) do | for _, line in ipairs(data.lines) do | ||
local color = line.color or '#888888' | local color = line.color or '#888888' | ||
| Строка 215: | Строка 287: | ||
local isFuture = (st.future == true) | local isFuture = (st.future == true) | ||
local node = canvas:tag('div') | local node = canvas:tag('div') | ||
:css({ | :css({ | ||
| Строка 233: | Строка 304: | ||
node:wikitext('[[' .. article .. '|<span style="display:block;width:100%;height:100%;" title="' .. name .. '"></span>]]') | node:wikitext('[[' .. article .. '|<span style="display:block;width:100%;height:100%;" title="' .. name .. '"></span>]]') | ||
local isRightSide = (x >= (origW / 2)) | |||
local isRightSide = (x >= | |||
local label = canvas:tag('div') | local label = canvas:tag('div') | ||
:css({ | :css({ | ||