A modern web application for live visualization of ship movements on Lake Zurich. The app displays ships in real-time on an interactive map based on timetable data from ZSG (Zürichsee Schifffahrtsgesellschaft).
- Live Tracking: Real-time tracking of all active ships on Lake Zurich
- Interactive Map: Leaflet-based map with OpenStreetMap (free, open source)
- Simulation Mode: Time-based simulation with speed controls (1x, 2x, 4x, 10x, 100x), always uses today's date
- Ship Details: Display of ship names, course numbers, departure and arrival times
- Route Visualization: Precise routes based on GeoJSON data
- Intelligent Position Calculation: Accounts for slower speed at arrival/departure
- Smart Ship Deduplication: Prevents duplicate ship displays when transitioning between routes
- Course Number Handling: Properly distinguishes between different courses (e.g., course 29 vs 2529)
- MS Albis Highlight: Special marking for the flagship MS Albis
- Bilingual: German and English interface
- Dark Mode: Automatic theme switching
- Mobile Optimized: Responsive design for all screen sizes
- User Documentation: Built-in documentation modal explaining how the app works
- Next.js 15 - React Framework with App Router
- TypeScript - Type safety
- Tailwind CSS - Utility-first CSS framework
- Leaflet - Open-source mapping library
- react-leaflet - React wrapper for Leaflet
- lucide-react - Modern icon library
- transport.opendata.ch API - Public transit timetable data
- ZSG Ships API - Ship names and course numbers
- Node.js 18+ and npm
- No API keys required! All used APIs are publicly accessible
- Clone the repository:
git clone <repository-url>
cd shipvisualization- Install dependencies:
npm install- Set up environment variables (optional):
Create a
.env.localfile in the root directory:
NEXT_PUBLIC_ZSG_API_URL=https://vesseldata-api.vercel.app/api/shipsThe NEXT_PUBLIC_ZSG_API_URL is optional and defaults to the VesselData API. If you want to use your own instance, you can adjust the URL here.
- Start the development server:
npm run devThe application will run on http://localhost:3000
npm run build
npm startThe project is optimized for Vercel:
- Vercel CLI:
npm i -g vercel
vercel-
GitHub Integration:
- Push repository to GitHub
- In Vercel Dashboard: "New Project" → Select GitHub repository
- Vercel automatically detects Next.js and configures the project
-
Environment Variables in Vercel:
- In Vercel Dashboard: Settings → Environment Variables
- Optional: Add
NEXT_PUBLIC_ZSG_API_URL
├── app/ # Next.js App Router
│ ├── api/ # API Routes (Proxies)
│ │ ├── ships/ # ZSG Ships API Proxy
│ │ └── stationboard/ # Transport API Proxy
│ ├── layout.tsx # Root Layout
│ ├── page.tsx # Main Page
│ └── globals.css # Global Styles
├── components/ # React Components
│ ├── ShipMap.tsx # Map Component
│ ├── SchedulePanel.tsx # Ship List & Details
│ ├── Documentation.tsx # User Documentation Modal
│ ├── Footer.tsx # Footer Component
│ └── ThemeLanguageToggle.tsx # Theme & Language Switcher
├── lib/ # Utilities and Logic
│ ├── transport-api.ts # Transport API Client
│ ├── ship-position.ts # Position Calculation
│ ├── ship-names-api.ts # Ship Names API Integration
│ ├── geojson-routes.ts # GeoJSON Route Loader
│ ├── zurichsee-stations.ts # Station Coordinates
│ ├── i18n.ts # Internationalization
│ ├── i18n-context.tsx # i18n React Context
│ └── theme.tsx # Theme Management
├── public/
│ └── data/
│ └── export.geojson # Ship Routes (GeoJSON)
└── package.json
The app loads ship routes from a GeoJSON file (public/data/export.geojson) that contains maritime route data:
The GeoJSON file was created using Overpass Turbo (https://overpass-turbo.eu/), a web-based data mining tool for OpenStreetMap:
-
Data Source:
- Routes are extracted from OpenStreetMap's OpenSeaMap layer
- OpenSeaMap contains detailed maritime navigation data including ferry routes
- Routes are marked as
route=ferryin OpenStreetMap
-
Export Process:
- Used Overpass Turbo to query ferry routes in the Lake Zurich area
- Query filters for routes with
route=ferrytag within the lake boundaries - Exported results as GeoJSON format
- The exported file contains
LineStringandMultiLineStringgeometries
-
Route Properties:
- Each route includes metadata:
name: Route name (e.g., "3732: Personenfähre Thalwil - Küsnacht - Erlenbach")ref: Course number reference@id: OpenStreetMap relation/way ID
- Coordinates are stored in GeoJSON format
[longitude, latitude]
- Each route includes metadata:
-
Loading:
- Loads
LineStringandMultiLineStringgeometries from the file - Converts coordinates from
[longitude, latitude](GeoJSON format) to[latitude, longitude](Leaflet format) - Extracts route metadata (name, ref, course numbers) from properties
- Each route segment is stored with its coordinate path
- Loads
-
Route Storage:
- Routes are cached server-side for 24 hours using Next.js
unstable_cache - Routes are loaded once on application start
- Each route contains an array of coordinates forming the path
MultiLineStringgeometries are split into separate route segments
- Routes are cached server-side for 24 hours using Next.js
When a ship travels between two stations, the app finds the best matching route from the GeoJSON data:
-
Proximity Search:
- Finds the nearest point on each route to the departure station
- Finds the nearest point on each route to the arrival station
- Uses Haversine formula to calculate distances
-
Scoring System:
- Ideal Match: Both stations within 500m of route endpoints (highest priority)
- Good Match: Both stations within 1km of route endpoints
- Fallback Match: Both stations within 5km of route endpoints
- Semantic Bonus: Route name contains station names or course number
- Course Number Bonus: Route metadata matches the ship's course number
-
Route Selection:
- Selects route with best combined score (distance + semantic matching)
- Automatically determines route direction (forward or reverse)
- Extracts the segment between the matched points
-
Fallback:
- If no route is found, uses linear interpolation between stations
- This should rarely happen if GeoJSON data is complete
Ship positions are calculated based on timetable data and route geometry:
-
Timetable Data from
transport.opendata.ch:- Departure time from origin station
- Arrival time at destination station
- Pass list with all intermediate stops and their times
- Course numbers for route matching
-
Time-Based Progress:
- Calculates elapsed time since departure
- Calculates total journey duration
- Determines progress ratio (0.0 = departure, 1.0 = arrival)
-
Non-Linear Speed Profile: The app uses a realistic speed profile that accounts for slower speeds at stations:
-
Phase 1 - Departure (first 0.5km):
- Speed: 6 knots (11 km/h)
- Accounts for acceleration and maneuvering
-
Phase 2 - Cruising (middle section):
- Speed: 12 knots (22 km/h)
- Normal travel speed
-
Phase 3 - Arrival (last 0.5km):
- Speed: 6 knots (11 km/h)
- Accounts for deceleration and docking
The progress calculation weights these phases differently:
- Approach phases take twice as long per kilometer as cruising
- This creates realistic acceleration/deceleration curves
-
-
Position Interpolation:
-
If a GeoJSON route is found:
- Calculates total route distance
- Finds the segment where the ship currently is
- Interpolates position along the route path
- Calculates heading (course) based on route direction
-
If no route is found (fallback):
- Uses linear interpolation between stations
- Calculates bearing (direction) from departure to arrival
-
-
Station Dwell Time:
- Ships are displayed at stations during dwell time
- Dwell time is calculated from arrival to next departure
- Ships are shown at the station location with status "at_station"
- Prevents duplicate display when ship is in transit
The app includes intelligent logic to prevent duplicate ship displays and correctly handle course numbers:
-
Segment Linking:
- When a ship arrives at a station and departs again, the segments are automatically linked
- The arrival time of the previous segment becomes the
arrivalAtFromStationof the next segment - This ensures smooth transitions without duplicate displays
-
Course Number Preservation:
- Full course numbers are preserved (e.g., "029", "2529") without truncation
- This prevents conflicts between different courses (e.g., course 29 vs course 2529)
- Ships are uniquely identified by
${shipName}|${fullCourseNumber}
-
Deduplication Logic:
- Ships are deduplicated based on ship name and full course number
- Priority rules: driving ships take precedence over stationary ships
- When multiple segments are active, the temporally closest one is selected
-
Ship Name Matching:
- Exact matching only (no aggressive "endsWith" matching)
- Prevents false matches between similar course numbers
- Ensures correct ship assignment for each course
- Server-Side Caching: 6 hours for timetable data (refreshes every 6 hours for new daily schedules)
- Client-Side Caching: 6 hours for ship names
- Multi-Layer Cache: In-Memory Cache + Next.js unstable_cache + Fetch Cache
- Rate-Limiting: Automatic retry logic for API limits
- Shows current ship movements in real-time
- Automatic updates every 1-2 minutes
- Time-based simulation with manual time control (always uses today's date)
- Speed controls: 1x, 2x, 4x, 10x, 100x (for fast-forwarding through the day)
- Timeline slider to scrub through the day
- Time picker to jump to specific times
- Reset button to reset to 13:32
- Automatically switches to 13:32 when entering simulation mode
- No API Keys in Code: All used APIs are public
- No User Data: The app does not collect personal data
- CORS Handling: API proxies safely bypass CORS restrictions
- Rate-Limiting: Automatic limitation of API requests
-
transport.opendata.ch: Public transit timetable data
- Endpoint:
/v1/stationboard - Documentation: https://transport.opendata.ch/
- Endpoint:
-
ZSG Ships API: Ship names and course numbers
- Endpoint:
/api/ships - Default URL:
https://vesseldata-api.vercel.app/api/ships
- Endpoint:
/api/ships- Proxy for ZSG Ships API/api/stationboard- Proxy for Transport API with caching
- The app uses automatic caching and retry logic
- For repeated errors: Wait a few minutes
- Check browser console for errors
- Ensure APIs are reachable
- In simulation mode: Check if time is set correctly
- Check internet connection
- OpenStreetMap tiles are publicly available, no API keys needed
This project is private and not intended for public use.
lakeshorestudios - https://lakeshorestudios.ch/
Made with AI 🤖
- transport.opendata.ch for the public timetable data
- OpenStreetMap for the free map tiles
- ZSG (Zürichsee Schifffahrtsgesellschaft) for the timetable data