Tells you whether you can park at your current location in Prague. There is no server and no backend: your location stays on your phone.
For the spot you're standing on, the app shows:
- whether it's in a paid parking zone, and if so which kind (visitors, mixed or residents)
- the price and paid hours right now, with a button to pay for that zone
- whether the street is closed for street cleaning today or soon
- the street name and zone code, so you can check them against the signs
GPS is only accurate to a few metres, so the app looks at every zone and street-cleaning section within that distance of you. Each one gets a card with its street and zone. When they all have the same rules (price, hours, time limit, cleaning), the app answers plainly. When they differ, it says it can't tell which one you're in, and each card shows what applies there.
"Live" in the top left corner shows that the answer follows your location, with bars for how accurate it is. Tap it to pause: the answer stays with the spot you paused at, for example the car's, while you walk away.
In a shared car (pick "Shared car" in the top right corner), it tells you whether you can end the rental there instead. Cars from carsharing services registered with the city can be left in any blue or purple zone for free, with no time limit, but not in orange zones.
With a parking permit (pick "Own car + permit"), you pick which areas your permits are for: a whole district such as Praha 5, or one of the smaller sub-areas some districts have, such as 5.1. In the blue and purple zones of those areas the app says you can park at will; the rest are answered as for any own car. A zone on the border between two areas counts for both. "Edit permits…" in the same menu changes them.
On a motorbike (pick "Motorbike"), every zone is free, with no time limit: motorbikes park without a permit in blue, purple and orange zones alike.
The app's icon in the top right corner opens the About page, with a link to the source and, where the browser can install the app from a button, Install (the Web Install API, or Chromium's own install prompt).
From Prague's open data catalog, LKOD:
- Zóny placeného stání vymezené tarifem – paid parking zones: their shapes and codes, and a text copy of their tariffs
- Letní údržba komunikací TSK – summer street cleaning (blokové čištění), with the cleaning dates for each street
From ČÚZK, the Czech land survey office:
- RÚIAN – Ulice – street centre lines and names from RÚIAN, the national address register
From Golemio, Prague's data platform:
- Parking tariffs from TSK (
/v3/parkingand/v3/parking-tariffs, sourcetsk_v2) – each zone's prices, paid hours, caps, and how long a visitor may stay
From VPH, the city's official parking map:
- Parking areas (
/api/v1/parking/area?category=PARKING) – the shapes of the districts and sub-areas that parking permits are issued for.
- Once a day, a script downloads the data, keeps only what the app needs, makes it smaller, and saves it as JSON files in this repo.
- The first time you open the app, your phone downloads these files and stores them.
- After that, the app looks up your location on the phone itself. It only goes online to check whether newer data is available, and it works offline too.
pipeline/– Node + TypeScript scripts that download and clean the data, then write it toweb/public/data/.web/– the app: Preact + TypeScript + Vite, installable as a PWA.
cd pipeline
npm install
npm run build # downloads the sources and writes web/public/data/*.json
Golemio needs an API key. The build reads it from GOLEMIO_API_KEY, which it takes from
the environment or from a .env file at the repo root (git ignores it):
GOLEMIO_API_KEY=...
What npm run build does:
- Downloads each source: the LKOD and RÚIAN layers as GeoJSON, TSK's zones and tariffs from Golemio, and the parking areas from VPH.
- Rounds and simplifies the shapes, and keeps only the fields the app uses.
- Stores repeated values (tariffs, cleaning dates, street names) once, in a shared table, and points to them from each shape.
- Writes one file per dataset, plus
manifest.jsonwith:version– a hash of the dataset sizes, so it only changes when the data doesbounds– a rough box around all the data, which the app uses to tell whether you're anywhere near Prague
The output files are committed to the repo. Nothing is downloaded when the app is built or deployed.
The datasets:
zps– paid parking zones. One shape per zone from LKOD, with its category (RES,MIXorVIS) and a tariff. The tariff comes from TSK's tariffs in Golemio, matched by zone code:- Each paid window becomes a rule with its days, hours and hourly price. Golemio charges by the minute, so the price is the per-minute charge × 60.
- A Golemio "maximum" becomes the rule's daily cap only when it actually limits what a stay would cost. Most maximums are just the price × the longest allowed stay.
- On a public holiday, a tariff works as on any other day, except that one stay may cost
no more than its holiday cap (
holidayCapCzk, 20 or 40 Kč). That's Golemio's holiday-only maximum. Most purple and orange zones have one; blue zones and the rest have none, and their holidays are ordinary days. The app works out the holidays itself (web/src/holidays.ts): the fixed dates, plus Good Friday and Easter Monday from Easter. - Windows never run past midnight. "Po-Pá 08:00-05:59" means 00:00–05:59 and 08:00–23:59 on each of Monday to Friday, so a Friday night into Saturday is free.
- For a zone Golemio doesn't have, the tariff is parsed from the LKOD
tariftextfield. That field is cut off at a fixed length, so a few long tariffs lose their daily cap; the build logs a warning for each one and keeps the rest of the tariff. - The build logs how many zones got their tariff from each place.
- Resident zones also get
maxStayMinutes, the longest a visitor may stay, from Golemio, matched by zone code. It'snullfor a section Golemio doesn't have, and the build logs how many those are. - Blue and purple zones also get the parking areas whose permits cover them (
areasId, pointing intoareaSets, e.g.["5", "5.1", "5.3"]): every VPH area whose shape holds the section's centre. Neighbouring areas overlap along their border streets, so a section there gets both. A section outside every district's shape gets the district its code names, and the build logs it. - The build fails if VPH can't be reached, its answer looks different from what it expects, or more than a few blue and purple sections end up with no area. The nightly refresh then fails without committing anything, so the app keeps the last good data.
letni– summer street cleaning. One shape per street section, with its list of cleaning days. The source only covers the current year, so this needs a refresh at least once a year.streets– street centre lines with their names, used to show which street you're on.
.forgejo/workflows/refresh-data.yaml runs the pipeline every night on the Forgejo runner,
with the Golemio key from the repo's GOLEMIO_API_KEY secret:
- Runs
npm run build. - Compares the new
manifest.jsonversion with the old one. - If it changed, commits the new data and pushes it to both Forgejo and GitHub. If not, it does nothing.
cd web
npm install
npm run dev # local dev server
npm run build # production build to web/dist
npm run preview # serve the production build locally
npm run test # unit tests (vitest)
How the app uses the data:
- On each start it fetches
manifest.json. If the version matches what it already has, it uses its stored copy (in IndexedDB). If not, it downloads the datasets again. - If it can't reach the network, it uses the stored copy.
- The lookups run in a web worker, using an R-tree index built from the data.
A test page for the app that runs it without real GPS. It shows the app in a phone-sized frame, next to a map with a pin:
cd web
npm run jig # opens http://localhost:5173/jig/
- Click the map or drag the pin to move the location. The slider sets the accuracy radius, as if it came from a GPS fix.
- Time is either Now, which follows the real clock, or Custom, a date and time you pick, to see prices and paid hours at other times. Times are in your computer's time zone, which is also the one the app reads tariff hours in.
- The Location menu switches to the other states the GPS can be in: still searching, gave up, permission denied, or no geolocation at all.
- The map draws the app's own data under the pin: paid zones in their category colours with their codes, and, when switched on, street-cleaning sections. These are coloured by when they're next cleaned, counted from the jig's day: red on that day (the app says not to park), amber within the app's warning window (it gives a heads-up), grey later.
- "Street cleaning everywhere that day" makes the app treat every street-cleaning section as cleaned on the chosen day, to see its closure screen anywhere with a section.
- The Screen menu sets the frame to a few common phone sizes. The frame shrinks to fit a shorter window, but the app still lays out at the phone's real size.
- The pin, accuracy and custom time are kept in the URL (
#lat,lon,accuracy[,time]), so a spot can be bookmarked or shared.
How it works:
Apptakes the location as a prop, and optionally the time and the street-cleaning simulation. The real app passes it the browser's GPS and leaves the rest to their defaults (src/main.tsx). The jig runs the sameAppin an iframe (jig/frame.tsx) and sends it the jig's settings withpostMessage.- The iframe gives the app its own viewport, so it lays out the same as on a phone.
- The jig lives in
web/jig/and is only served by the dev server. It is not part ofnpm run build, and its map library (MapLibre) is a dev dependency. - The map tiles come from OpenFreeMap, which is free, needs no API key and has no usage limits. The map data is © OpenStreetMap contributors.
pipeline/andweb/each havenpm run test.- Tests sit next to the code they cover, as
<module>.test.ts. They are not part of the app build or the pipeline output. - Formatting is Prettier, set up once at the repo root for both folders:
npm install # at the repo root
npm run format # fix formatting
npm run format:check # check only
.github/workflows/deploy.ymlbuildsweb/and deploysweb/distto GitHub Pages on every push tomain.- The domain
canipark.smeral.netis set inweb/public/CNAME, with a DNS CNAME record pointing torsmeral.github.io.
- The app icon (
web/public/icons/icon.png) is the logo of the original 2017 project, taken from theoldbranch.icon-maskable.pngis the same logo filling the whole square, for launchers that crop icons into their own shape. - The status emoji (
web/public/emoji/*.svg) are Twemoji (CC-BY 4.0). They are shown as images because the phone's own emoji look blurry at this size.