Map Data Loading and Rendering
The application efficiently loads, filters, and renders stop markers on the interactive map using spatial queries, caching, and marker diffing.
Data Pipeline
| Step | Component | Purpose |
|---|---|---|
| 1 | PostGIS | Spatial index queries via GiST |
| 2 | API | /api/data with viewport bounds |
| 3 | Cache | Avoid redundant requests |
| 4 | Diff | Update only changed markers |
PostGIS Spatial Query
SELECT * FROM stops_matched
WHERE ST_Intersects(
geom,
ST_MakeEnvelope(min_lon, min_lat, max_lon, max_lat, 4326)
)
The real /api/data query also includes a second branch for matched rows whose OSM point is inside the viewport even when the ATLAS-side display geometry is outside it. The geom column has a GiST spatial index for the primary lookup path.
Zoom Level Modes
| Zoom | Mode | Behavior |
|---|---|---|
| < 14 | Overview / Low Zoom | Unmatched ATLAS only (if no filters) or capped filtered results |
| 14–15 | Capped / Mid Zoom | All types (or filters), limited to a capped count to maintain performance |
| ≥ 16 | Full / Uncapped Zoom | All matched markers in viewport naturally rendered without clipping |
Zoom Banner Policy
The application intelligently restricts the number of rendered markers to maintain performance, and communicates this state via the map's zoom banner.
- High Zoom (≥ 16): The banner is hidden. The data isn't artificially capped.
- Mid-Zoom (14-15): Results are capped (currently 1,800 matches). If filters are inactive, the banner advises the user to zoom in. If filters are active, it indicates that results are capped (e.g.,
Showing first 1800 filtered results...). If the real query size naturally drops below the cap, the banner is hidden. - Low Zoom (< 14): By default, the map runs an "Overview mode" displaying only unmatched ATLAS components to save resources. The banner indicates this mode. If explicit active filters are applied, the map respects the filter instead of Overview mode, applies a heavy cap, and updates the banner accordingly.
- Top N Filters: Bypasses all zoom-based warning banners, since the data slice is targeted and strictly constrained natively.
Frontend Caching
Cache reuse logic:
- Zoom in + previous NOT capped → Reuse (new viewport is subset)
- Zoom out or pan beyond bounds → Fetch new data
- Filters changed → Fetch new data
Implementation details:
- viewport cache stores
bounds, stable non-bboxkey,zoom,data, andcappedstate - cache key is built from filter params plus mode and excludes bbox-only params
- the cache is invalidated explicitly on filter changes via
invalidateViewportCache()
Cache ownership boundaries:
- this cache applies only to
/api/dataviewport payload reuse in the browser /api/global_statsuses a separate backend cache (in-process LRU) and is not served from viewport cache- both caches are driven by the same active filter semantics, so requests stay logically aligned even though cache layers are separate
Marker Diffing
Instead of recreating all markers:
- Track markers by key (
atlas-{sloid}orosm-{node_id}) - Existing: Update position with
setLatLng() - New: Create and add to map
- Stale: Remove from map
Benefits: No flickering, smooth transitions, efficient DOM operations
Canvas vs DOM Markers
| Zoom | Marker Type | Implementation |
|---|---|---|
| < 18 | CircleMarker | Canvas-based, lightweight |
| ≥ 18 | L.Marker | DOM-based, supports icons |
At zoom 18, markers are recreated to switch rendering modes.
Labeled Marker Policy (D / P / S)
When marker rendering is in DOM mode (zoom >= 18), selected marker categories render a lettered icon:
| Side | Condition | Label |
|---|---|---|
| ATLAS | ATLAS stop is member of duplicate_group_sloids |
D |
| OSM | osm_node_type = platform |
P |
| OSM | osm_node_type = railway_station |
S |
All other marker categories keep the standard circle style.
Marker Colors
| Type | Color | CSS Class |
|---|---|---|
| ATLAS (unmatched) | Green | atlas-marker |
| OSM (unmatched) | Orange | osm-marker |
| Matched (ATLAS) | Green with indicator | matched-atlas |
| Matched (OSM) | Orange with indicator | matched-osm |
Line Rendering Thresholds
| Line Type | Min Zoom |
|---|---|
| ATLAS↔OSM matched connection lines | 13 |
| OSM group / trio connector lines | 17 |
Overlap Handling
The MarkerClusterManager handles stops at identical coordinates:
// Coordinates rounded to tolerance grid
const key = `${Math.round(lat * 1e6)}_${Math.round(lon * 1e6)}`;
// Overlapping markers offset in circle pattern
const angle = (index / total) * 2 * Math.PI;
const offset = radius * { x: Math.cos(angle), y: Math.sin(angle) };
This prevents markers from stacking invisibly.
Map Layers
The interactive map provides 3 layer options.
| Layer | URL |
|---|---|
| OpenStreetMap | tile.openstreetmap.org |
| Transport | tile.memomaps.de |
| Satellite | arcgisonline.com |
Code Reference
| File | Purpose |
|---|---|
| backend/blueprints/data.py | API endpoint |
| static/js/pages/main.js | Map initialization |
| static/js/components/map-renderer.js | Marker rendering |
| matching_and_import_db/database/importer.py | Geometry import |
Related Documentation
- 5. Web app – Application overview
- 4. Database – Database schema