Модуль:ТранспортнаяСхема: различия между версиями

Нет описания правки
мНет описания правки
Строка 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 wrapper = mw.html.create('div')
   
     -- Внешний контейнер-обёртка
     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'] = string.format('%.1fpx', finalBoxW),
             ['width'] = '100%',
             ['height'] = string.format('%.1fpx', finalBoxH),
             ['height'] = string.format('%.1fpx', finalBoxH),
            ['max-width'] = '100%',
             ['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',
            ['margin'] = (args['прижатие'] == 'лево' and '0 auto 0 0') or (args['прижатие'] == 'право' and '0 0 0 auto') or '0 auto',
             ['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'] = origW .. 'px',
             ['width'] = string.format('%.1fpx', origW * (isScrollMode and scale or 1.0)),
             ['height'] = origH .. 'px',
             ['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 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


     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 x1, y1 = parseCoords(st1.coords)
                 local rawX1, rawY1 = parseCoords(st1.coords)
                 local x2, y2 = parseCoords(st2.coords)
                 local rawX2, rawY2 = parseCoords(st2.coords)
                  
                  
                 if x1 and y1 and x2 and y2 then
                 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 - 3) .. 'px',
                             ['top'] = (y1 - trackHeight / 2) .. 'px',
                             ['width'] = length .. 'px',
                             ['width'] = length .. 'px',
                             ['height'] = isFuture and '0px' or '6px',
                             ['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 ('5px dashed ' .. color) or 'none',
                             ['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 x, y = parseCoords(st.coords)
                 local rawX, rawY = parseCoords(st.coords)
                 if x and y then
                 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 - 7) .. 'px',
                             ['left'] = (x - nodeSize / 2) .. 'px',
                             ['top'] = (y - 7) .. 'px',
                             ['top'] = (y - nodeSize / 2) .. 'px',
                             ['width'] = '14px',
                             ['width'] = nodeSize .. 'px',
                             ['height'] = '14px',
                             ['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 = (x >= (origW / 2))
                    -- Текстовая подпись
                     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 + 11) .. 'px') or ((x - 11) .. 'px'),
                             ['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'] = '11.5px',
                             ['font-size'] = fontSize .. 'px',
                             ['font-weight'] = '500',
                             ['font-weight'] = '500',
                             ['z-index'] = '4',
                             ['z-index'] = '4',
Строка 326: Строка 372:
     end
     end


     return tostring(wrapper)
     return tostring(root)
end
end


return p
return p