Модуль:ТранспортнаяСхема: различия между версиями
Tringoli (обсуждение | вклад) Нет описания правки |
Tringoli (обсуждение | вклад) мНет описания правки |
||
| Строка 94: | Строка 94: | ||
end | end | ||
-- Генерация интерактивной Canvas-схемы с панорамированием | -- Генерация интерактивной Canvas-схемы с зумом и панорамированием | ||
function p.canvas(frame) | function p.canvas(frame) | ||
local args = getArgs(frame) | local args = getArgs(frame) | ||
| Строка 102: | Строка 102: | ||
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 | ||
| Строка 123: | Строка 123: | ||
local boxW = tonumber(args['ширина_блока'] or args['ширина_карточки'] or args['ширина']) | local boxW = tonumber(args['ширина_блока'] or args['ширина_карточки'] or args['ширина']) | ||
local scale = tonumber(args['масштаб'] or args['scale']) | local scale = tonumber(args['масштаб'] or args['scale'] or args['зум'] or args['zoom']) | ||
local isScrollMode = (args['прокрутка'] == 'да' or args['скролл'] == 'да' or args['scroll'] == 'yes') | local isScrollMode = (args['прокрутка'] == 'да' or args['скролл'] == 'да' or args['scroll'] == 'yes') | ||
local showZoomButtons = (args['кнопки_зума'] ~= 'нет' and (isScrollMode or scale ~= nil)) | |||
if not isScrollMode and boxW and not scale then | if not isScrollMode and boxW and not scale then | ||
| Строка 133: | Строка 134: | ||
end | end | ||
local finalBoxW = boxW or (targetW * scale) | local finalBoxW = boxW or (targetW * (isScrollMode and 1.0 or scale)) | ||
local finalBoxH = tonumber(args['высота_блока'] or args['высота_карточки'] or args['высота']) or (targetH * scale) | local finalBoxH = tonumber(args['высота_блока'] or args['высота_карточки'] or args['высота']) or (targetH * (isScrollMode and 1.0 or scale)) | ||
-- Внешний контейнер- | local uid = 'ts-map-' .. tostring(math.random(10000, 99999)) | ||
local | |||
-- Внешний контейнер-обёртка | |||
local root = mw.html.create('div') | |||
:addClass('ts-transport-map-container') | |||
:css({ | |||
['position'] = 'relative', | |||
['width'] = string.format('%.1fpx', finalBoxW), | |||
['max-width'] = '100%', | |||
['margin'] = (args['прижатие'] == 'лево' and '0 auto 0 0') or (args['прижатие'] == 'право' and '0 0 0 auto') or '0 auto' | |||
}) | |||
-- Панель управления (Зум и навигация) | |||
if showZoomButtons or isScrollMode then | |||
local toolbar = root:tag('div') | |||
:addClass('ts-map-toolbar') | |||
:css({ | |||
['position'] = 'absolute', | |||
['top'] = '10px', | |||
['right'] = '10px', | |||
['z-index'] = '20', | |||
['display'] = 'flex', | |||
['align-items'] = 'center', | |||
['gap'] = '4px', | |||
['background'] = 'rgba(255, 255, 255, 0.94)', | |||
['border'] = '1px solid #d0d7de', | |||
['border-radius'] = '20px', | |||
['padding'] = '3px 8px', | |||
['box-shadow'] = '0 2px 6px rgba(0,0,0,0.08)', | |||
['font-size'] = '11px', | |||
['color'] = '#57606a', | |||
['backdrop-filter'] = 'blur(4px)', | |||
['user-select'] = 'none' | |||
}) | |||
toolbar:tag('span') | |||
:css({['font-weight'] = 'bold', ['margin-right'] = '3px', ['font-size'] = '10px'}) | |||
:wikitext('🔍 Зум:') | |||
local zoomLevels = { {label = '75%', val = 0.75}, {label = '100%', val = 1.0}, {label = '140%', val = 1.4}, {label = '180%', val = 1.8} } | |||
for _, z in ipairs(zoomLevels) do | |||
local checked = (math.abs(scale - z.val) < 0.05) | |||
toolbar:tag('span') | |||
:addClass('ts-zoom-btn') | |||
:css({ | |||
['padding'] = '1px 6px', | |||
['border-radius'] = '10px', | |||
['background'] = checked and '#0969da' or '#f6f8fa', | |||
['color'] = checked and '#ffffff' or '#24292f', | |||
['font-weight'] = checked and 'bold' or 'normal', | |||
['border'] = '1px solid ' .. (checked and '#0969da' or '#d0d7de'), | |||
['cursor'] = 'pointer', | |||
['font-size'] = '10px' | |||
}) | |||
:wikitext(z.label) | |||
end | |||
end | |||
-- Окно просмотра (Viewport) | |||
local wrapper = root:tag('div') | |||
:addClass('ts-transport-map-viewport') | :addClass('ts-transport-map-viewport') | ||
:attr('tabindex', '0') | :attr('tabindex', '0') | ||
:attr('title', ' | :attr('title', 'Управление: стрелки клавиатуры (← ↑ → ↓), колесо мыши / тачпад для панорамирования') | ||
:css({ | :css({ | ||
['position'] = 'relative', | ['position'] = 'relative', | ||
['width'] = | ['width'] = '100%', | ||
['height'] = string.format('%.1fpx', finalBoxH), | ['height'] = string.format('%.1fpx', finalBoxH), | ||
['overflow'] = isScrollMode and 'auto' or 'hidden', | ['overflow'] = isScrollMode and 'auto' or 'hidden', | ||
['scroll-behavior'] = 'smooth', | ['scroll-behavior'] = 'smooth', | ||
| Строка 152: | Строка 210: | ||
['border-radius'] = '10px', | ['border-radius'] = '10px', | ||
['border'] = args['без_рамки'] and 'none' or '1px solid #d0d7de', | ['border'] = args['без_рамки'] and 'none' or '1px solid #d0d7de', | ||
['background-color'] = '#ffffff', | ['background-color'] = '#ffffff', | ||
['background-image'] = 'radial-gradient(#e1e4e8 1px, transparent 1px)', | ['background-image'] = 'radial-gradient(#e1e4e8 1px, transparent 1px)', | ||
| Строка 158: | Строка 215: | ||
['box-shadow'] = args['без_тени'] and 'none' or '0 2px 8px rgba(0,0,0,0.06)', | ['box-shadow'] = args['без_тени'] and 'none' or '0 2px 8px rgba(0,0,0,0.06)', | ||
['outline'] = 'none', | ['outline'] = 'none', | ||
['cursor'] = isScrollMode and 'grab' or 'default', | |||
['user-select'] = 'none' | ['user-select'] = 'none' | ||
}) | }) | ||
| Строка 164: | Строка 222: | ||
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') | ||
| Строка 171: | Строка 229: | ||
['left'] = not isScrollMode and string.format('%.1fpx', -shiftX * scale) or '0', | ['left'] = not isScrollMode and string.format('%.1fpx', -shiftX * scale) or '0', | ||
['top'] = not isScrollMode and string.format('%.1fpx', -shiftY * scale) or '0', | ['top'] = not isScrollMode and string.format('%.1fpx', -shiftY * scale) or '0', | ||
['width'] = | ['width'] = string.format('%.1fpx', origW * (isScrollMode and scale or 1.0)), | ||
['height'] = | ['height'] = string.format('%.1fpx', origH * (isScrollMode and scale or 1.0)), | ||
['transform'] = (scale ~= 1.0 and not isScrollMode) and string.format('scale(%.4f)', scale) or 'none', | ['transform'] = (scale ~= 1.0 and not isScrollMode) and string.format('scale(%.4f)', scale) or 'none', | ||
['transform-origin'] = '0 0', | ['transform-origin'] = '0 0', | ||
| Строка 214: | Строка 272: | ||
end | end | ||
local drawScale = isScrollMode and scale or 1.0 | |||
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' | ||
| Строка 247: | Строка 282: | ||
for i = 1, #stations - 1 do | for i = 1, #stations - 1 do | ||
local st1, st2 = stations[i], stations[i+1] | local st1, st2 = stations[i], stations[i+1] | ||
local | local rawX1, rawY1 = parseCoords(st1.coords) | ||
local | local rawX2, rawY2 = parseCoords(st2.coords) | ||
if x1 | if rawX1 and rawY1 and rawX2 and rawY2 then | ||
local x1, y1 = rawX1 * drawScale, rawY1 * drawScale | |||
local x2, y2 = rawX2 * drawScale, rawY2 * drawScale | |||
local dx = x2 - x1 | local dx = x2 - x1 | ||
local dy = y2 - y1 | local dy = y2 - y1 | ||
| Строка 256: | Строка 294: | ||
local angle = math.deg(math.atan2(dy, dx)) | local angle = math.deg(math.atan2(dy, dx)) | ||
local isFuture = (st1.future and st2.future) | local isFuture = (st1.future and st2.future) | ||
local trackHeight = math.max(3, math.floor(6 * math.min(1.4, drawScale))) | |||
canvas:tag('div') | canvas:tag('div') | ||
| Строка 261: | Строка 300: | ||
['position'] = 'absolute', | ['position'] = 'absolute', | ||
['left'] = x1 .. 'px', | ['left'] = x1 .. 'px', | ||
['top'] = (y1 - | ['top'] = (y1 - trackHeight / 2) .. 'px', | ||
['width'] = length .. 'px', | ['width'] = length .. 'px', | ||
['height'] = isFuture and '0px' or ' | ['height'] = isFuture and '0px' or (trackHeight .. 'px'), | ||
['background-color'] = isFuture and 'transparent' or color, | ['background-color'] = isFuture and 'transparent' or color, | ||
['border-top'] = isFuture and (' | ['border-top'] = isFuture and ((trackHeight - 1) .. 'px dashed ' .. color) or 'none', | ||
['transform-origin'] = '0 50%', | ['transform-origin'] = '0 50%', | ||
['transform'] = 'rotate(' .. angle .. 'deg)', | ['transform'] = 'rotate(' .. angle .. 'deg)', | ||
| Строка 281: | Строка 320: | ||
for _, st in ipairs(line.stations or {}) do | for _, st in ipairs(line.stations or {}) do | ||
local | local rawX, rawY = parseCoords(st.coords) | ||
if x | if rawX and rawY then | ||
local x, y = rawX * drawScale, rawY * drawScale | |||
local name = st.name or st.article or '' | local name = st.name or st.article or '' | ||
local article = st.article or name | local article = st.article or name | ||
local isFuture = (st.future == true) | local isFuture = (st.future == true) | ||
local nodeSize = math.max(10, math.floor(14 * math.min(1.3, drawScale))) | |||
-- Кружок станции | |||
local node = canvas:tag('div') | local node = canvas:tag('div') | ||
:css({ | :css({ | ||
['position'] = 'absolute', | ['position'] = 'absolute', | ||
['left'] = (x - | ['left'] = (x - nodeSize / 2) .. 'px', | ||
['top'] = (y - | ['top'] = (y - nodeSize / 2) .. 'px', | ||
['width'] = ' | ['width'] = nodeSize .. 'px', | ||
['height'] = ' | ['height'] = nodeSize .. 'px', | ||
['border-radius'] = '50%', | ['border-radius'] = '50%', | ||
['background-color'] = '#ffffff', | ['background-color'] = '#ffffff', | ||
| Строка 304: | Строка 346: | ||
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 = ( | -- Текстовая подпись | ||
local isRightSide = (rawX >= (origW / 2)) | |||
local fontSize = math.max(10, math.floor(11.5 * math.min(1.3, drawScale))) | |||
local offset = math.floor(11 * drawScale) | |||
local label = canvas:tag('div') | local label = canvas:tag('div') | ||
:css({ | :css({ | ||
['position'] = 'absolute', | ['position'] = 'absolute', | ||
['left'] = isRightSide and ((x + | ['left'] = isRightSide and ((x + offset) .. 'px') or ((x - offset) .. 'px'), | ||
['top'] = y .. 'px', | ['top'] = y .. 'px', | ||
['transform'] = isRightSide and 'translateY(-50%)' or 'translate(-100%, -50%)', | ['transform'] = isRightSide and 'translateY(-50%)' or 'translate(-100%, -50%)', | ||
['text-align'] = isRightSide and 'left' or 'right', | ['text-align'] = isRightSide and 'left' or 'right', | ||
['white-space'] = 'nowrap', | ['white-space'] = 'nowrap', | ||
['font-size'] = ' | ['font-size'] = fontSize .. 'px', | ||
['font-weight'] = '500', | ['font-weight'] = '500', | ||
['z-index'] = '4', | ['z-index'] = '4', | ||
| Строка 326: | Строка 372: | ||
end | end | ||
return tostring( | return tostring(root) | ||
end | end | ||
return p | return p | ||