# Swiss Transport — the panel in detail Everything the panel does, and the reasoning where it is not obvious. For what it is and how to install it, see the [README](../README.md). ## Getting a key The plugin will not make a single request until you give it an Open Journey Planner key, and the federal platform issues one **free of charge to anyone who asks** — there is no cost, no approval step and no quota to negotiate. It takes about two minutes: 1. Register at [api-manager.opentransportdata.swiss](https://api-manager.opentransportdata.swiss/) with an email address. 2. Subscribe your account to the **OJP 2.0** API. 3. Copy the token it shows you. 4. Open the panel and paste it into the field. The free tier is 50 requests a minute and 20 000 a day, which is far more than this plugin uses — it holds itself below both, deliberately, with a token bucket checked before every request rather than by trusting its own timers. A plugin cannot ship a shared key on your behalf. It would be a public secret in a public repository, and the first person to misuse it would get it revoked for everyone at once. Yours is yours. ### Where it is kept ~/.local/state/omarchy/plugins/jmaeder.swisstransport/curlrc Created with `install -m 600` inside a directory made `0700` — with the final permissions from the moment it exists, rather than written and then chmod-ed, so there is no window in which it is readable. It is written in a form `curl` reads directly, so the key never appears in a command line where `ps` would show it. The panel never reads it back. What it checks is whether the file exists, not what is in it, which is why a key that has been revoked shows up as a failed request rather than as a warning. **Forget key** in the footer deletes the file. ## What it shows ![The panel: the map, the mode ticks and the footer](panel.png) **In the bar** — a locomotive with a Swiss cross laid over its bottom-right corner in the current theme's colours. Nothing else: no count, no next departure. A transport map is something you consult, not something you monitor out of the corner of an eye, and the plugin stays silent and offline until you open it. **In the panel** - A **map** centred on you, or on any Swiss stop or town you search for. Stops are drawn as rings, vehicles as moving marks. - **Drag to pan, wheel to zoom** — the wheel zooms about the pointer, so what you are leaning towards stays under it. Buttons in the corner do the same for anyone without a wheel. It opens on a square kilometre and goes from 200 m to 20 km; the extent is remembered. - **Favourite stops in the header**, alongside the one detection found. Pin wherever the map is, or a stop from its own card, and click a chip to go back to it. The **★** button in the header opens the full list — every saved place with its distance from where the map is now, and a remove control that is actually visible, which the strip's hover-only ✕ is not. Each favourite is starred on the map too, so a saved place is findable without reading the strip. A chip shows the stop's **whole** name and its canton — "Le Grand-St-Bernard (VS), Hospice" — taking a full row if that is what the name needs, because half a name is not a place. See *Two names for one stop* below for why the whole name is harder to get than it sounds. - **Clickable stops.** A stop opens a card with its departure board — wait, clock time, line, destination, platform and delay — and, above it, **every line that calls there at all**. That second list is the interchange: it says whether this is a stop where one bus passes or one where four tram lines and two buses meet, which is a difference a dot on a map cannot show. Clicking a departure follows that vehicle; clicking a **boxed line number**, in either list, draws that whole line across the map. The two are deliberately different actions on the same row — "where does the 213 go" and "where is this particular bus" are different questions, and you can have both answers on screen at once. - Every vehicle carries **the glyph of what it is** — train, metro, tram, bus, boat, cable car, funicular — with a heading pip showing which way it is going. Colour is only ever a second signal, because a theme is free to make two of its accents nearly identical. - **Ticks for each mode**, so you can put the buses away and watch the trains. - Click a vehicle and you get its **number, type, derived speed, status, and the stops immediately behind and ahead of it** — with *Not applicable* where there genuinely is no previous stop (at an origin) or next one (at a terminus). Its **route is drawn on the map**, so "next stop: Kreuzplatz" comes with where Kreuzplatz actually is. - **Real-time or timetable-only** is stated per vehicle, because a vehicle *reported* to be somewhere and a vehicle *believed* to be there are not the same claim. - Two mountain presets, **I'm hiking!** and **I'm skiing!** — see below for what they honestly are. - A language switch: **EN / FR / DE**. It covers the plugin's own words and is also sent to the planner, which returns stop names and product categories in it. - The **swisstopo national map** faintly under everything, grey by default with a tick for the colour sheet. - **An empty map says which kind of empty it is.** Away from the cities the common case is a timetable with nothing on it — route 213 up the Trient valley runs six times a day, so at eleven in the morning there is genuinely nothing to draw. A map that answers that with silence has told you your software is broken, so it names the next departure instead. "No vehicles in range" is kept for what it actually means: stops that reported nothing, or a view you have filtered down to nothing. ## Where it starts Detection can fail, be switched off, or place you somewhere this plugin cannot serve. Those are three different situations and none of them has a right answer, so here is what each one does. | Situation | Where the map opens | | --- | --- | | A saved centre | Exactly where you left off. | | Detection finds you in Switzerland or Liechtenstein | The nearest stop to you, at any distance. | | Detection finds you elsewhere, within 30 km of the border | The nearest Swiss stop — Annemasse gets Genève, Konstanz gets Kreuzlingen. | | Detection finds you elsewhere, further away | Zürich HB. | | Detection fails | Zürich HB, and it tries again next session. | | `detectLocation` is off | Zürich HB, and no request is ever made. | The country decides the rule, not the coordinate, and the mechanism is the one [jmaeder.swissweather](https://github.com/jmaeder/omarchy-swissweather) uses: a covered country searches for the nearest stop with **no distance limit**, anywhere else is capped at 30 km. Liechtenstein counts as covered and not as a courtesy — its whole bus network is in the Swiss service point register, so Vaduz, Schaan and Triesenberg answer with ordinary `ch:1:sloid:` references, and treating an LI address as foreign would apply a border limit to somebody standing at a stop this plugin can name. A missing or unrecognised country code is read as abroad, which costs a border-limited search rather than an unlimited one from a place that cannot be served. Detection is attempted at most **once per shell session**, on top of the persisted flag. A first run with the network down, or against a rate-limited ipapi.co, retries next session rather than being written off. Detection runs **once and is then kept**, because an IP address is not a thing to poll. But people move, and a location from a previous trip is worse than no answer: it is a confident wrong one. So the pin chip in the header carries a **↻** — press it and the plugin looks you up again and moves the map there. A lookup that fails costs nothing: the view you were on stays. That control is not a convenience. The old code recorded "detection has been done" on the first line of its response handler, *before* looking at the response. ipapi.co rate-limits by address and answers 429 readily, and with `curl --fail` that arrives as exit 22 and an empty body — so one throttled request at the wrong moment switched detection off for the life of the install, and the map opened at Zürich HB forever with nothing on screen to say why. The flag now records that the question has been *answered*, not that it has been asked, and the two cases that count as answered are "here is where you are" and "here is where you are, and it is too far from Switzerland to help" — the second because asking again would return the same thing. The border limit is what keeps the courtesy honest: past thirty kilometres the nearest Swiss stop stops describing anywhere you could be catching a bus, and a confident wrong answer is worse than an obvious placeholder you can search away from. Milan is 42 km from the nearest Swiss point and stays out. Zürich HB is the placeholder for a concrete reason rather than a patriotic one: it is the busiest station in the country, so the map opens on something *moving*. A quieter default would open on an empty square, and an empty square is indistinguishable from a broken plugin on the one screen where you have no idea yet what working looks like. ## The two mountain presets *I'm hiking!* and *I'm skiing!* change what the panel is, not just what it filters. What each can honestly show was measured against the live services across twelve places in nine cantons before any of it was built — the audit, with its numbers, is in [mountain-modes-feasibility.md](mountain-modes-feasibility.md). ### The transport half Both presets narrow the transport to what gets you to and from the mountain: skiing to cable cars and funiculars, hiking to those plus boats and trains. The narrowing now reaches the sampling as well as the drawing. Only a handful of stops can be polled per cycle — each one is a request — and they used to be chosen spread across the view regardless of mode. At a resort that meant six stops of which perhaps two were lifts, so a map announcing mountain lifts was mostly showing buses. Stops that serve the chosen modes are picked first now, and spread across the view second: the same six requests, spent on the right stops. Where nothing serves those modes the filter steps aside rather than emptying the map, because a valley with only buses is better shown with its buses than with nothing. **A lift that is shut reads as shut.** The planner answers `STOPEVENT_NOEVENTFOUND` for a stop with nothing calling, which is the correct answer for half the lifts around Davos in August. That used to raise the failure banner every twenty seconds over a panel that was working perfectly; it is now an empty board with a line saying so. ### The terrain half Click the map itself — not a stop, not a vehicle — and the panel answers what is *there*: - **The altitude**, anywhere in the country, from the federal terrain model. The service answers only in the national grid, so the coordinate is converted first by swisstopo's published approximation, which is arithmetic and costs no request. Checked against Lake Brienz, which came back 563.7 m against an official 564. - **The routes at that point.** Hiking gives the signposted network — named, numbered, with the stage endpoints, "Via Alpina (Altdorf (UR) – Engelberg)", route 1. Skiing gives ski-touring routes with the club's own grade, the ascent in metres, the published climbing time, the summit or hut each one leads to with its altitude, and a link to the route's page. Nothing here polls. Terrain does not change while you look at it, and a service answering "which routes are here" is not one to ask sixty times a minute. The search radius follows the zoom: pointing at a 20 km view means something looser than pointing at a 200 m one. Worth knowing if you touch this code — the identify endpoint's `tolerance` is in **screen pixels**, not metres, so it only means a fixed ground distance if the extent and display size in the same request are chosen to make it one. Set to a fixed 25 px it worked at Engelberg and silently found nothing at Andermatt, where the routes start two hundred metres out. ### What the presets do not claim **Marked piste names do not exist as open data.** Of 880 layers in the federal catalogue, two mention lifts and none covers pistes; the winter national map draws them as pixels with nothing behind them to ask. Resorts hold that information and do not publish it. The ski card says so in as many words, because every winter map shows pistes and a card that shows ski routes and no pistes would otherwise look broken rather than bounded. Mountain restaurants are not published either, and the SAC hut network is not available as a layer — huts appear only as the *destination* of a ski-touring route, with their altitude and a link. ### The one link this plugin did not write A ski route answers with its page on the Swiss Alpine Club's portal, and for deciding whether a tour is for you that page is the substance. It is also a URL from a remote response heading for the desktop's URL opener, which is the one field in a response that can act on the machine it lands on. So it gets its own door rather than widening the existing one: `Net.sacRouteCommand` accepts https only, one host, a bounded length, and a path of URL characters and nothing else. The rule for the plugin's own fixed pages — verbatim, or nothing — is untouched. **I'm skiing!** and **I'm hiking!** do two things at once. They filter the transport — skiing to cable cars and funiculars, hiking to those plus boats and trains, the modes that get you to a trailhead and back — and they draw the real thing underneath: - **Skiing** adds `ch.swisstopo-karto.skitouren`, the national **ski-touring** map: the marked routes, with their direction of travel. - **Hiking** adds `ch.swisstopo.swisstlm3d-wanderwege`, the swissTLM3D **footpath network** — the paths themselves, everywhere in the country, not only the signposted national routes. Two honest limits. These are ski *touring* routes, not groomed downhill pistes; swisstopo does not publish those and neither does anyone else as open data, so the tick does not claim them. And with `basemap: false` no swisstopo request is made at all, so the presets fall back to being mode filters and the panel says so under the ticks rather than silently doing less. ## Zooming out, and what thins Vehicles come from the stops the plugin polls, and each polled stop is a request — so their number is bounded by `maxStops` however far you zoom out. That bound is the honest limit of zooming out, and it is worth knowing which way it was resolved. Watching simply the *nearest* stops is right at a kilometre and wrong at ten: every watched stop ends up in the middle of the view and the whole rim of the map has no traffic on it, which reads as missing data rather than as sampling. So the watched stops are chosen **spread across the view** instead — nearest to the middle first, then repeatedly whichever remaining stop is furthest from the ones already chosen. Same number of requests, traffic visible across the whole view. The *drawn* stop network does scale with the extent, because that is a single request whatever it returns. So a wide view shows the shape of the network properly, with live vehicles sampled across it rather than clustered. ## Two names for one stop Ask the planner for `Le Grand-St-Bernard, Hospice` by name and it answers: Le Grand-St-Bernard, Hospice Le Grand-St-Bernard, Hospice (Bourg-St-Pierre) Ask it for the stops within three kilometres of that same point, and the same stop comes back as: Le Grand-St-Bernard, Hospice Hospice `` is written for the answer, not for the stop: a geographic query assumes you already know which valley you are looking at, so it drops the locality. That is reasonable in a list of nearby stops and useless in a favourite, where "Hospice" is a word with no place attached — and every favourite pinned from the map got that form. `` is the same string in both, so it is what this plugin stores and draws. The canton is a separate problem, and the planner cannot solve it. A stop carries a `TopographicPlaceRef` of `23024032:2` and a `TopographicPlaceName` of `Bourg-St-Pierre`; asking for topographic places directly returns the same bare municipality. Nothing in any OJP response names a canton, and the numeric reference does not decode to one — Sion and Bourg-St-Pierre are both Valais and their references share no field. So the canton comes from swisstopo's swissBOUNDARIES3D, which is the authoritative answer to exactly that question, free, and needs no key. It is asked once per place and kept. It goes after the locality rather than at the end, because that is what it belongs to: `Le Grand-St-Bernard (VS), Hospice`, not `Le Grand-St-Bernard, Hospice (VS)`. The second reads as "the Hospice of canton Valais", which is a different and wrong claim. ## Settings Set these on the widget's entry in `~/.config/omarchy/shell.json`. | Key | Default | What it does | | --- | --- | --- | | `language` | system locale | `en`, `fr` or `de`. The panel's own selector writes here. | | `swissCross` | `true` | The Swiss cross badge on the bar locomotive. | | `sideMetres` | `1000` | Side of the square of ground the map covers, so 1000 is 1 km × 1 km. Clamped to 200–20000. The panel's own zoom writes here. | | `renderSeconds` | `0.2` | How often positions are re-interpolated on screen. This is the whole of the animation, so it is what decides whether a vehicle glides or hops. Costs no request. Fractional, clamped to 0.1–10. | | `networkSeconds` | `20` | How often the planner is polled. Clamped to 10–300, and further limited by the quota guard. | | `maxStops` | `6` | How many nearby stops are queried per cycle. More stops, more vehicles, more requests. Clamped to 1–12. | | `detectLocation` | `true` | One geolocation request on first run, to centre the map. Turn off to keep every request on opentransportdata.swiss. | | `basemap` | `true` | Kill switch, not a mode. False stops every swisstopo request, tiles and cantons both. | | `basemapLayer` | `grey` | `grey` or `colour`. The panel's own tick writes here. An unrecognised value falls back to grey. | | `basemapOpacity` | per layer | How strongly it shows through, 0.05–0.8. Left unset, each layer uses its own strength — the colour map is drawn fainter than the grey one, because its colours compete with the markers where grey ink does not. |