%% tikzlibrarytrainmap.code.tex %% ========================================================================= %% trainmap -- core TikZ library for drawing schematic railway / transit maps %% (in the style of FGC / Rodalies line diagrams). %% %% This file contains the actual implementation. It is loaded by %% \usetikzlibrary{trainmap} %% and the LaTeX wrapper `trainmap.sty' calls that for you when you write %% \usepackage{trainmap} %% %% Copyright (C) 2026 Pol Sardà Palla %% This work may be distributed and/or modified under the conditions of the %% LaTeX Project Public License, version 1.3c or (at your option) any later %% version. See LICENSE for the full text. Maintainer: Pol Sardà Palla. %% ========================================================================= % TikZ libraries used internally: % calc -> coordinate arithmetic, e.g. ($(a)+(0,1)$) % arrows.meta -> nicer arrow tips (available to the user) % bending -> arrow tips that bend along curved paths \usetikzlibrary{calc,arrows.meta,bending} % ========================================================================= % 1. COLOUR PALETTE % Official line colours of Rodalies de Catalunya and FGC (hex values). % You may of course use any xcolor colour (red, blue!60!black, ...). % ========================================================================= % --- Rodalies de Catalunya --- \definecolor{tmR1}{HTML}{4499D4} % R1 light blue \definecolor{tmR2}{HTML}{009900} % R2 green \definecolor{tmR2N}{HTML}{99C83E} % R2 Nord lime green \definecolor{tmR2S}{HTML}{00642E} % R2 Sud dark green \definecolor{tmR3}{HTML}{FF131A} % R3 red \definecolor{tmR4}{HTML}{FF9221} % R4 orange \definecolor{tmR7}{HTML}{BD7DB5} % R7 lilac \definecolor{tmR8}{HTML}{9B1987} % R8 purple / magenta \definecolor{tmR11}{HTML}{0064A5} % R11 dark blue \definecolor{tmR12}{HTML}{FFDC00} % R12 yellow \definecolor{tmR13}{HTML}{E52E87} % R13 fuchsia pink \definecolor{tmR14}{HTML}{675199} % R14 violet \definecolor{tmR15}{HTML}{9A8A76} % R15 beige grey / brown \definecolor{tmR16}{HTML}{AF0036} % R16 maroon \definecolor{tmR17}{HTML}{E97300} % R17 dark orange \definecolor{tmRL1}{HTML}{6B8E23} % RL1 olive green \definecolor{tmRL2}{HTML}{32CD32} % RL2 bright lime green \definecolor{tmRG1}{HTML}{007DC3} % RG1 cyan blue \definecolor{tmRT1}{HTML}{35BDB2} % RT1 turquoise \definecolor{tmRT2}{HTML}{DC87B9} % RT2 light pink % --- FGC, Barcelona-Valles line --- \definecolor{tmL6}{HTML}{847DC6} % L6 violet \definecolor{tmL7}{HTML}{AE6118} % L7 brown \definecolor{tmL12}{HTML}{B6B3E1} % L12 very light lilac \definecolor{tmS1}{HTML}{F47216} % S1 orange \definecolor{tmS2}{HTML}{59C134} % S2 green % --- FGC, Llobregat-Anoia line --- \definecolor{tmL8}{HTML}{E659B4} % L8 pink / magenta \definecolor{tmR5}{HTML}{4FC4BB} % R5 turquoise \definecolor{tmR50}{HTML}{007D9B} % R50 petrol blue \definecolor{tmR6}{HTML}{9EA1A6} % R6 grey \definecolor{tmR60}{HTML}{565A5C} % R60 dark grey % ========================================================================= % 2. CONFIGURATION KEYS (pgfkeys, namespace /trainmap/) % Every key can be set globally (on the environment) or locally (in the % optional argument of an individual command). % ========================================================================= \pgfkeys{ /trainmap/.is family, /trainmap/.cd, % --- Tracks / lines -------------------------------------------------- line color/.initial=black, % default colour of a single track track width/.initial=2pt, % stroke width of a track % --- Map style ------------------------------------------------------- mode/.initial=fgc, % fgc | rodalies (see the manual) line code/.initial={}, % single-line code drawn at the two ends % (rodalies mode); auto-filled from the % first \addterminal if left empty % --- Standard stations ---------------------------------------------- station color/.initial=black, % colour of the perpendicular tick mark station width/.initial=1.8pt, % stroke width of the tick mark % --- Terminal / head stations --------------------------------------- terminal size/.initial=8pt, % diameter of the terminal disc terminal border/.initial=1.4pt, % border width of the terminal disc badge color/.initial=black, % background colour of the line badge % --- Interchange stations (capsules) -------------------------------- capsule width/.initial=12pt, % outer width of the interchange capsule capsule inner/.initial=8pt, % inner (white) width of the capsule capsule color/.initial=black, % border colour of the capsule % --- Geometry of the running route ---------------------------------- station step/.initial=1.6, % horizontal distance between stations (cm) track sep/.initial=0.16, % vertical gap between parallel tracks (cm) % --- Serpentine wrapping (for long lines) --------------------------- max width/.initial=0, % wrap to a new row past this x (cm); 0 = off row sep/.initial=2.6, % vertical distance between wrapped rows (cm) % --- Station labels -------------------------------------------------- label angle/.initial=45, % rotation of the label text (degrees) label position/.initial=above, % above | below (label over or under track) % --- Correspondences ------------------------------------------------- corr/.initial={}, % list "CODE/colour, CODE/colour, ..." of % connecting lines, shown as small % coloured chips below the station % --- Internal -------------------------------------------------------- name/.initial={}, % optional id -> creates named coordinates } % ========================================================================= % 3. TikZ STYLES DERIVED FROM THE KEYS % ========================================================================= \tikzset{ % A standard track: configurable width, rounded ends. trainmap track/.style={ line width=\pgfkeysvalueof{/trainmap/track width}, line cap=round, }, % The perpendicular tick mark of a standard station. trainmap station/.style={ line width=\pgfkeysvalueof{/trainmap/station width}, line cap=round, color=\pgfkeysvalueof{/trainmap/station color}, }, % The filled disc of a terminal / head station. trainmap terminal/.style={ circle, draw, line width=\pgfkeysvalueof{/trainmap/terminal border}, minimum size=\pgfkeysvalueof{/trainmap/terminal size}, inner sep=0pt, fill=white, }, % Rotated label placed ABOVE the track (reads towards the upper right). % `align=left' makes manual line breaks (\\) work inside station names. trainmap label above/.style={ rotate=\pgfkeysvalueof{/trainmap/label angle}, anchor=south west, font=\footnotesize, align=left, inner sep=2pt, }, % Rotated label placed BELOW the track (reads towards the lower left). trainmap label below/.style={ rotate=\pgfkeysvalueof{/trainmap/label angle}, anchor=north east, font=\footnotesize, align=left, inner sep=2pt, }, % Line badge: rounded box with the line code in white (fgc mode). trainmap badge/.style={ rounded corners=1.5pt, inner xsep=3.5pt, inner ysep=1.5pt, font=\scriptsize\bfseries, align=center, text=white, }, % Square line-code plate drawn at the ends of a single line (rodalies mode). trainmap linecode/.style={ rectangle, draw=none, inner sep=3pt, minimum height=13pt, font=\footnotesize\bfseries, align=center, text=white, }, % Small correspondence chip: a coloured square with the connecting line code. trainmap chip/.style={ rectangle, draw=none, inner sep=1.5pt, minimum width=9pt, minimum height=9pt, font=\tiny\bfseries, align=center, text=white, }, } % ========================================================================= % 4. INTERNAL STATE (the running "cursor") % ------------------------------------------------------------------------- % \tm@x current horizontal position of the cursor (number, cm) % \tm@first flag "1" until the first station has been placed % \tm@lanes list "y/colour, y/colour, ..." of the parallel trunk tracks % \tm@ttop top y of the trunk band (relative, before row mapping) % \tm@tbot bottom y of the trunk band (relative, before row mapping) % \tm@center vertical centre of the trunk band (for serpentine mirroring) % \tm@dir travelling direction: +1 (rightwards) or -1 (leftwards) % \tm@sign parity of the current row: +1 (normal) or -1 (mirrored) % \tm@rowbase y of the current row's centre (0, -rowsep, -2*rowsep, ...) % \tmlastx (public) x of the last placed station -> anchor for branches % \tmlasty (public) y (centre) of the last placed station % % Row mapping (supports the serpentine layout). The absolute y of a trunk % track stored as `l' is: % y(l) = (rowbase + center) + sign * (l - center) % On row 0 this reduces to y = l, so non-wrapped maps keep their absolute % coordinates and hand-placed branch endpoints stay valid. On mirrored rows % the track order flips, which is exactly what a parallel U-turn requires so % that the tracks nest concentrically without crossing. % ========================================================================= \def\tm@x{0} \def\tm@first{1} \def\tm@lanes{0/black} \def\tm@ttop{0} \def\tm@tbot{0} \def\tm@center{0} \def\tm@dir{1} \def\tm@sign{1} \def\tm@rowbase{0} \def\tm@mode{fgc} \def\tm@linecode{} \def\tm@lanecolor{black} \def\tm@rodalies{rodalies} % constant used for \ifx comparisons % ---- \settrunk{} ----------------------------------------- % Declare the parallel tracks that make up the common trunk, and compute the % band extent and centre. May be called again at any time to add or remove % tracks (e.g. when a line branches off and leaves the trunk). % % \settrunk{0/tmS2, 0.18/tmS1, 0.36/tmL7, 0.54/tmL6} \newcommand{\settrunk}[1]{% \gdef\tm@lanes{#1}% \def\tm@mina{1000}\def\tm@maxb{-1000}% \foreach \ly/\lc in {#1}{% \pgfmathparse{min(\tm@mina,\ly)}\xdef\tm@mina{\pgfmathresult}% \pgfmathparse{max(\tm@maxb,\ly)}\xdef\tm@maxb{\pgfmathresult}% \xdef\tm@lanecolor{\lc}% remember a track colour (for the end plates) }% \global\let\tm@tbot\tm@mina \global\let\tm@ttop\tm@maxb \pgfmathsetmacro{\tm@cc}{(\tm@mina+\tm@maxb)/2}\xdef\tm@center{\tm@cc}% } % ---- \tm@drawsegment{}{} : draw one trunk segment on current row -- \newcommand{\tm@drawsegment}[2]{% \pgfmathsetmacro{\tm@rcs}{\tm@rowbase + \tm@center}% \foreach \ly/\lc in \tm@lanes {% \pgfmathsetmacro{\tm@yy}{\tm@rcs + \tm@sign*(\ly - \tm@center)}% \draw[trainmap track, draw=\lc] (#1,\tm@yy) -- (#2,\tm@yy);% }% } % ---- \tm@dowrap : perform a serpentine U-turn to the next row ------------ % Draws, for every trunk track, a concentric half-loop connecting the end of % the current row to the start of the row below, then flips direction and % row parity. The half-loops nest automatically because their radii grow % with the track's distance from the band centre. \newcommand{\tm@dowrap}{% \pgfmathsetmacro{\tm@rs}{\pgfkeysvalueof{/trainmap/row sep}}% \pgfmathsetmacro{\tm@nb}{\tm@rowbase - \tm@rs}% % next row base (below) \pgfmathsetmacro{\tm@ns}{-\tm@sign}% % next row parity \pgfmathsetmacro{\tm@rcA}{\tm@rowbase + \tm@center}% % this row centre (abs) \pgfmathsetmacro{\tm@rcB}{\tm@nb + \tm@center}% % next row centre (abs) \foreach \ly/\lc in \tm@lanes {% \pgfmathsetmacro{\tm@yA}{\tm@rcA + \tm@sign*(\ly - \tm@center)}% \pgfmathsetmacro{\tm@yB}{\tm@rcB + \tm@ns*(\ly - \tm@center)}% \ifdim \tm@dir pt>0pt % travelling right -> loop on the right-hand side \draw[trainmap track, draw=\lc] (\tm@x,\tm@yA) to[out=0,in=0,looseness=1.35] (\tm@x,\tm@yB);% \else % travelling left -> loop on the left-hand side \draw[trainmap track, draw=\lc] (\tm@x,\tm@yA) to[out=180,in=180,looseness=1.35] (\tm@x,\tm@yB);% \fi }% \xdef\tm@rowbase{\tm@nb}% \pgfmathsetmacro{\tm@nsx}{-\tm@sign}\xdef\tm@sign{\tm@nsx}% \pgfmathsetmacro{\tm@ndx}{-\tm@dir}\xdef\tm@dir{\tm@ndx}% } % ---- \tm@advance : move the cursor to the next station ------------------ % On the first station: stay at x=0. Otherwise move by `station step' in the % current direction, wrapping to a new row first if the move would exceed % `max width' (when serpentine wrapping is enabled). Finally compute the % row-aware geometry used to draw the station: % \tm@ytop \tm@ybot extent of the trunk band on this row % \tm@a \tm@b the tick mark endpoints (band extent + margin) % \tm@mid vertical centre (disc / capsule position) \newcommand{\tm@advance}{% \ifnum\tm@first=1 \gdef\tm@first{0}% \else \pgfmathsetmacro{\tm@mw}{\pgfkeysvalueof{/trainmap/max width}}% \pgfmathsetmacro{\tm@newx}{\tm@x + \tm@dir*(\pgfkeysvalueof{/trainmap/station step})}% \ifdim \tm@mw pt>0pt % serpentine wrapping enabled -> test whether we must turn \pgfmathparse{((\tm@dir>0)&&(\tm@newx>\tm@mw))||((\tm@dir<0)&&(\tm@newx<0))}% \ifdim \pgfmathresult pt>0.5pt \tm@dowrap % turn; x stays at the row edge \else \tm@drawsegment{\tm@x}{\tm@newx}% \xdef\tm@x{\tm@newx}% \fi \else \tm@drawsegment{\tm@x}{\tm@newx}% \xdef\tm@x{\tm@newx}% \fi \fi \global\let\tmlastx\tm@x \pgfmathsetmacro{\tm@rc}{\tm@rowbase + \tm@center}% \pgfmathsetmacro{\tm@ytop}{\tm@rc + \tm@sign*(\tm@ttop - \tm@center)}% \pgfmathsetmacro{\tm@ybot}{\tm@rc + \tm@sign*(\tm@tbot - \tm@center)}% \pgfmathsetmacro{\tm@a}{min(\tm@ytop,\tm@ybot)-0.16}% \pgfmathsetmacro{\tm@b}{max(\tm@ytop,\tm@ybot)+0.16}% \pgfmathsetmacro{\tm@mid}{(\tm@ytop+\tm@ybot)/2}% \global\let\tmlasty\tm@mid } % ---- \tm@namecoord : expose named coordinates if name= was given -------- \newcommand{\tm@namecoord}{% \pgfkeysgetvalue{/trainmap/name}{\tm@nm}% \ifx\tm@nm\@empty\else \coordinate (\tm@nm) at (\tm@x,\tm@ytop);% top track \coordinate (\tm@nm-bot) at (\tm@x,\tm@ybot);% bottom track \coordinate (\tm@nm-mid) at (\tm@x,\tm@mid);% band centre \fi } % ---- \tm@drawcorr : draw correspondence chips below the station --------- % Reads the `corr' key (list "CODE/colour, ...") and stacks a small coloured % square per connecting line just beneath the station. \newcommand{\tm@drawcorr}{% \pgfkeysgetvalue{/trainmap/corr}{\tm@cl}% \ifx\tm@cl\@empty\else \foreach \tm@cc/\tm@ccol [count=\tm@ci from 0] in \tm@cl {% \node[trainmap chip, fill=\tm@ccol, anchor=north] at (\tm@x, {\tm@a - 0.18 - \tm@ci*0.36}) {\tm@cc};% }% \fi } % ========================================================================= % 5. ENVIRONMENT % ========================================================================= % \begin{trainmap}[] ... \end{trainmap} % Wraps a tikzpicture and resets the cursor / trunk state for this map. \newenvironment{trainmap}[1][]{% \begingroup \gdef\tm@x{0}% \gdef\tm@first{1}% \gdef\tm@lanes{0/black}% \gdef\tm@ttop{0}\gdef\tm@tbot{0}\gdef\tm@center{0}% \gdef\tm@dir{1}\gdef\tm@sign{1}\gdef\tm@rowbase{0}% \gdef\tm@lanecolor{black}% \global\let\tmlastx\tm@x \global\let\tmlasty\tm@x \pgfkeys{/trainmap/.cd,#1}% \xdef\tm@mode{\pgfkeysvalueof{/trainmap/mode}}% \xdef\tm@linecode{\pgfkeysvalueof{/trainmap/line code}}% \begin{tikzpicture}[x=1cm,y=1cm]% }{% % In rodalies mode, close the single line with a square plate at its tail. \ifx\tm@mode\tm@rodalies \ifx\tm@linecode\@empty\else \pgfmathsetmacro{\tm@tx}{\tmlastx + \tm@dir*0.6}% \draw[trainmap track, draw=\tm@lanecolor] (\tmlastx,\tmlasty) -- (\tm@tx,\tmlasty);% \ifdim \tm@dir pt>0pt \node[trainmap linecode, fill=\tm@lanecolor, anchor=west] at (\tm@tx,\tmlasty) {\tm@linecode};% \else \node[trainmap linecode, fill=\tm@lanecolor, anchor=east] at (\tm@tx,\tmlasty) {\tm@linecode};% \fi \fi \fi \end{tikzpicture}% \endgroup } % ---- \tm@rodhead : draw the head plate at the very first station -------- % In rodalies mode the single line begins next to a square plate carrying the % line code, placed to the left of the first station. #1 is the "was first" % flag captured before \tm@advance. \newcommand{\tm@rodhead}[1]{% \ifx\tm@mode\tm@rodalies \ifnum#1=1 \ifx\tm@linecode\@empty\else \pgfmathsetmacro{\tm@hx}{\tm@x - 0.6}% \draw[trainmap track, draw=\tm@lanecolor] (\tm@hx,\tm@mid) -- (\tm@x,\tm@mid);% \node[trainmap linecode, fill=\tm@lanecolor, anchor=east] at (\tm@hx,\tm@mid) {\tm@linecode};% \fi \fi \fi } % ========================================================================= % 6. PUBLIC API -- STATIONS % ========================================================================= % ---- \addstation[]{} ---------------------------------------- % Standard station: a perpendicular tick mark crossing every parallel track % of the trunk, plus a rotated label. Handy keys: % name= create coordinates (), (-mid), (-bot) % label position=below put the label under the track % station step= use a custom gap before this station \newcommand{\addstation}[2][]{% \begingroup \pgfkeys{/trainmap/.cd,#1}% \edef\tm@wasfirst{\tm@first}% \tm@advance \draw[trainmap station] (\tm@x,\tm@a) -- (\tm@x,\tm@b);% \tm@rodhead{\tm@wasfirst}% \tm@namecoord \tm@drawcorr \pgfkeysgetvalue{/trainmap/label position}{\tm@lp}% \def\tm@posabove{above}% \ifx\tm@lp\tm@posabove \node[trainmap label above] at (\tm@x,\tm@b) {#2};% \else \node[trainmap label below] at (\tm@x,\tm@a) {#2};% \fi \endgroup } % ---- \addterminal[]{}{} -------------------------- % Terminal / head station: a filled disc in the line colour, the name in % bold, and a badge with the line code underneath. Choose the colour with % badge color=. \newcommand{\addterminal}[3][]{% \begingroup \pgfkeys{/trainmap/.cd,#1}% % In rodalies mode, adopt the first terminal's code as the line code % (used for the square plates at both ends) unless one was set explicitly. \ifx\tm@mode\tm@rodalies \ifx\tm@linecode\@empty \xdef\tm@linecode{#3}\fi \fi \edef\tm@wasfirst{\tm@first}% \tm@advance \pgfkeysgetvalue{/trainmap/badge color}{\tm@bc}% \node[trainmap terminal, fill=\tm@bc] at (\tm@x,\tm@mid) {};% \tm@rodhead{\tm@wasfirst}% \tm@namecoord \tm@drawcorr \node[trainmap label above, font=\footnotesize\bfseries] at (\tm@x,\tm@b) {#2};% % The line code goes below the disc only in fgc mode (several lines shown); % in rodalies mode the code lives on the end plates instead. \ifx\tm@mode\tm@rodalies\else \node[trainmap badge, fill=\tm@bc, anchor=north, yshift=-3pt] at (\tm@x,\tm@a) {#3};% \fi \endgroup } % ---- \addinterchange[]{} ------------------------------------ % Interchange / correspondence station: a rounded capsule that visually % groups all the parallel tracks of the trunk, with one coloured dot per % line. Use it where several services share a station (e.g. Sarria). \newcommand{\addinterchange}[2][]{% \begingroup \pgfkeys{/trainmap/.cd,#1}% \edef\tm@wasfirst{\tm@first}% \tm@advance \tm@rodhead{\tm@wasfirst}% \draw[line width=\pgfkeysvalueof{/trainmap/capsule width}, line cap=round, color=\pgfkeysvalueof{/trainmap/capsule color}] (\tm@x,\tm@ytop) -- (\tm@x,\tm@ybot);% \draw[line width=\pgfkeysvalueof{/trainmap/capsule inner}, line cap=round, color=white] (\tm@x,\tm@ytop) -- (\tm@x,\tm@ybot);% \foreach \ly/\lc in \tm@lanes {% \pgfmathsetmacro{\tm@yy}{\tm@rowbase + \tm@center + \tm@sign*(\ly - \tm@center)}% \fill[color=\lc] (\tm@x,\tm@yy) circle[radius=1.8pt];% }% \tm@namecoord \tm@drawcorr \node[trainmap label above, font=\footnotesize\bfseries] at (\tm@x,\tm@b) {#2};% \endgroup } % ========================================================================= % 7. PUBLIC API -- TRACKS, BRANCHES AND FORKS % ------------------------------------------------------------------------- % The / arguments are passed WITH their parentheses, so they may % be explicit coordinates {(\tmlastx,0.36)}, named nodes {(sarria)} or even % `calc' expressions {($(avtib)+(0,0.2)$)}. This lets you freely mix the % cursor engine with hand-placed geometry. % ========================================================================= % ---- \track[]{}{}{} : straight track segment ----- \newcommand{\track}[4][]{% \draw[trainmap track, draw=#4, #1] #2 -- #3;% } % ---- \branch[]{}{}{} : gentle horizontal branch -- \newcommand{\branch}[4][]{% \draw[trainmap track, draw=#4, #1] #2 to[out=0, in=180] #3;% } % ---- \forkup / \forkline : branch that peels UPWARDS --------------------- \newcommand{\forkline}[4][]{% \draw[trainmap track, draw=#4, #1] #2 to[out=30, in=180] #3;% } \newcommand{\forkup}[4][]{% \draw[trainmap track, draw=#4, #1] #2 to[out=30, in=180] #3;% } % ---- \forkdown : branch that peels DOWNWARDS ----------------------------- \newcommand{\forkdown}[4][]{% \draw[trainmap track, draw=#4, #1] #2 to[out=-30, in=180] #3;% } % ---- \joinline : a branch merging back INTO the trunk -------------------- \newcommand{\joinline}[4][]{% \draw[trainmap track, draw=#4, #1] #2 to[out=0, in=210] #3;% } \endinput .